@zhushanwen/pi-subagent-workflow 0.1.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/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/runtime/session-file-gc.ts
|
|
2
|
+
//
|
|
3
|
+
// 概率性清理过期 subagent session 文件(TTL 30 天)。
|
|
4
|
+
// session_start 时调用,best-effort(失败不影响启动)。
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { isProcessAlive, readAliveMarker } from "./alive-store.ts";
|
|
10
|
+
|
|
11
|
+
/** 30 天 TTL(毫秒)。 */
|
|
12
|
+
const TTL_DAYS = 30;
|
|
13
|
+
const HOURS_PER_DAY = 24;
|
|
14
|
+
const MINUTES_PER_HOUR = 60;
|
|
15
|
+
const SECONDS_PER_MINUTE = 60;
|
|
16
|
+
const MS_PER_SECOND = 1000;
|
|
17
|
+
const TTL_MS = TTL_DAYS * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
18
|
+
|
|
19
|
+
/** 触发概率(1/20 = 5%,避免每次 session_start 都全扫)。 */
|
|
20
|
+
const CLEANUP_PROBABILITY_DIVISOR = 20;
|
|
21
|
+
const CLEANUP_PROBABILITY = 1 / CLEANUP_PROBABILITY_DIVISOR;
|
|
22
|
+
|
|
23
|
+
/** subagent session 文件目录(相对 agentDir)。 */
|
|
24
|
+
const SUBAGENTS_DIR = "subagents";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 概率性清理过期 session 文件。best-effort——任何异常不外抛。
|
|
28
|
+
*
|
|
29
|
+
* 1. 概率性触发(CLEANUP_PROBABILITY)
|
|
30
|
+
* 2. 递归扫描 <agentDir>/subagents 下所有 .jsonl 文件
|
|
31
|
+
* 3. mtime 超 TTL → unlink
|
|
32
|
+
*/
|
|
33
|
+
export function maybeCleanupExpiredSessionFiles(agentDir: string, cwd: string): void {
|
|
34
|
+
void cwd;
|
|
35
|
+
try {
|
|
36
|
+
if (Math.random() >= CLEANUP_PROBABILITY) return;
|
|
37
|
+
const dir = path.join(agentDir, SUBAGENTS_DIR);
|
|
38
|
+
if (!fs.existsSync(dir)) return;
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
walkAndClean(dir, now);
|
|
41
|
+
} catch (_e) {
|
|
42
|
+
// best-effort:任何异常不阻断 session_start
|
|
43
|
+
void _e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 递归扫描目录,unlink 超 TTL 的 .jsonl 文件及其 .cancelled sidecar。 */
|
|
48
|
+
function walkAndClean(dir: string, now: number): void {
|
|
49
|
+
let entries: fs.Dirent[];
|
|
50
|
+
try {
|
|
51
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
} catch (_e) {
|
|
53
|
+
void _e;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
walkAndClean(full, now);
|
|
60
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.statSync(full);
|
|
63
|
+
if (now - stat.mtimeMs > TTL_MS) {
|
|
64
|
+
// .alive 探活:活进程的 session 不删(D-024 安全网)。
|
|
65
|
+
const aliveMarker = readAliveMarker(full);
|
|
66
|
+
if (aliveMarker !== undefined && isProcessAlive(aliveMarker.pid)) {
|
|
67
|
+
continue; // 活进程 → 跳过,不清理
|
|
68
|
+
}
|
|
69
|
+
fs.unlinkSync(full);
|
|
70
|
+
// 同名 sidecar 一起清理。
|
|
71
|
+
for (const ext of [".cancelled", ".finalized", ".alive"]) {
|
|
72
|
+
try { fs.unlinkSync(`${full}${ext}`); } catch (_e) { void _e; /* sidecar 可能不存在 */ }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
// 文件可能已被删除,忽略
|
|
77
|
+
void _e;
|
|
78
|
+
}
|
|
79
|
+
} else if (
|
|
80
|
+
entry.name.endsWith(".cancelled") ||
|
|
81
|
+
entry.name.endsWith(".finalized") ||
|
|
82
|
+
entry.name.endsWith(".alive") ||
|
|
83
|
+
// [MF#2] patch 回传(<branch>.patch)按 branch 命名,与 .jsonl basename 无关联,
|
|
84
|
+
// 删除 .jsonl 时无法同删;作为孤儿按 TTL 清理,避免永久堆积。
|
|
85
|
+
entry.name.endsWith(".patch")
|
|
86
|
+
) {
|
|
87
|
+
// 孤儿 sidecar(兄弟 .jsonl 已被外部删除):按同 TTL 清理。
|
|
88
|
+
try {
|
|
89
|
+
const stat = fs.statSync(full);
|
|
90
|
+
if (now - stat.mtimeMs > TTL_MS) {
|
|
91
|
+
fs.unlinkSync(full);
|
|
92
|
+
}
|
|
93
|
+
} catch (_e) {
|
|
94
|
+
void _e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// src/core/session-reconstructor.ts
|
|
2
|
+
//
|
|
3
|
+
// 从 subagent 的 session.jsonl 重建完整的 SubagentRecord 数据。
|
|
4
|
+
//
|
|
5
|
+
// 背景:subagent 执行时 SDK 把完整对话(assistant text/thinking/toolCall + usage +
|
|
6
|
+
// toolResult)实时 flush 进 session.jsonl。终态后内存 record 被立即淘汰(archive),
|
|
7
|
+
// collectRecords 读时用本模块从 session.jsonl 重建 turns[],恢复 eventLog/result/error
|
|
8
|
+
// 等富数据——session.jsonl 是唯一 source of truth(history.jsonl 已废弃)。
|
|
9
|
+
//
|
|
10
|
+
// 身份恢复:session.jsonl 的 header 不含 ExecutionRecord.id / agent / mode,故
|
|
11
|
+
// session-runner 在创建 session 后立即写一条 custom entry(customType:"subagent-identity")
|
|
12
|
+
// 携带 {id, agent, mode, task, startedAt}。本模块读这条 entry 恢复身份。
|
|
13
|
+
//
|
|
14
|
+
// 纯函数 + 防御性 I/O:任何文件缺失/损坏/格式漂移/缺 identity entry 均返回 undefined
|
|
15
|
+
// (不抛),消费方降级跳过该 record 而非崩溃。与 execution-record.ts 同层(Core 纯数据变换)。
|
|
16
|
+
//
|
|
17
|
+
// 直接读文件 + 逐行 JSON.parse(session.jsonl = newline-delimited JSON),不依赖 SDK
|
|
18
|
+
// SessionManager 类——避免 tsconfig paths fallback 把 class 降级为 interface 的坑。
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
AgentEventLogEntry,
|
|
24
|
+
AgentUsage,
|
|
25
|
+
ExecutionMode,
|
|
26
|
+
ExecutionStatus,
|
|
27
|
+
InternalToolCall,
|
|
28
|
+
Turn,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
import { extractLabelFromArgs } from "./execution-record.ts";
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// 类型(SDK jsonl 结构的最小子集——窄化为重建所需字段)
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
/** assistant message 的 usage(pi-ai Usage 的最小消费字段)。 */
|
|
37
|
+
interface JsonlUsage {
|
|
38
|
+
input?: number;
|
|
39
|
+
output?: number;
|
|
40
|
+
cacheRead?: number;
|
|
41
|
+
cacheWrite?: number;
|
|
42
|
+
cost?: { total?: number };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** assistant content block 联合(text/thinking/toolCall——重建只消费这三类)。 */
|
|
46
|
+
interface TextBlock { type: "text"; text: string; }
|
|
47
|
+
interface ThinkingBlock { type: "thinking"; thinking: string; }
|
|
48
|
+
interface ToolCallBlock { type: "toolCall"; id: string; name: string; arguments?: unknown; }
|
|
49
|
+
type AssistantContentBlock = TextBlock | ThinkingBlock | ToolCallBlock;
|
|
50
|
+
|
|
51
|
+
/** assistant message(pi-ai AssistantMessage 的最小消费字段)。 */
|
|
52
|
+
interface JsonlAssistantMessage {
|
|
53
|
+
role: "assistant";
|
|
54
|
+
content: AssistantContentBlock[];
|
|
55
|
+
usage?: JsonlUsage;
|
|
56
|
+
stopReason?: string;
|
|
57
|
+
errorMessage?: string;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** toolResult message(pi-ai ToolResultMessage 的最小消费字段)。 */
|
|
62
|
+
interface JsonlToolResultMessage {
|
|
63
|
+
role: "toolResult";
|
|
64
|
+
toolCallId: string;
|
|
65
|
+
toolName: string;
|
|
66
|
+
content?: unknown[];
|
|
67
|
+
details?: unknown;
|
|
68
|
+
isError?: boolean;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** user message(重建 turns[] 时跳过,仅用于类型穷尽)。 */
|
|
73
|
+
interface JsonlUserMessage {
|
|
74
|
+
role: "user";
|
|
75
|
+
content: unknown;
|
|
76
|
+
timestamp: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type JsonlMessage = JsonlUserMessage | JsonlAssistantMessage | JsonlToolResultMessage;
|
|
80
|
+
|
|
81
|
+
/** custom entry 的 data(session-runner 写入的 subagent-identity)。 */
|
|
82
|
+
export interface SubagentIdentityData {
|
|
83
|
+
id: string;
|
|
84
|
+
agent: string;
|
|
85
|
+
mode: ExecutionMode;
|
|
86
|
+
task: string;
|
|
87
|
+
startedAt: number;
|
|
88
|
+
/** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
|
|
89
|
+
rootSessionId?: string;
|
|
90
|
+
/** 直接父 subagent record ID。旧文件缺失 → undefined(顶层)。 */
|
|
91
|
+
parentRecordId?: string;
|
|
92
|
+
/** subagent 递归深度。旧文件缺失 → 0(顶层)。 */
|
|
93
|
+
depth?: number;
|
|
94
|
+
/** [MF#4] 本 session 的 fork 深度(session-runner 写入 parentForkDepth+1)。旧文件可能缺失。 */
|
|
95
|
+
forkDepth?: number;
|
|
96
|
+
/** @deprecated 兼容旧文件:旧 identity entry 写的是 parentSessionId,读取时 fallback 到 rootSessionId。 */
|
|
97
|
+
parentSessionId?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** SDK jsonl entry(getEntries() 返回,header 已排除)。 */
|
|
101
|
+
interface JsonlEntry {
|
|
102
|
+
type: string;
|
|
103
|
+
message?: JsonlMessage;
|
|
104
|
+
customType?: string;
|
|
105
|
+
data?: unknown;
|
|
106
|
+
/** model_change entry 的字段。 */
|
|
107
|
+
provider?: string;
|
|
108
|
+
modelId?: string;
|
|
109
|
+
/** thinking_level_change entry 的字段。 */
|
|
110
|
+
thinkingLevel?: string;
|
|
111
|
+
/** entry 级别的时间戳(ISO 字符串,SDK 逐行写入)。供 endedAt 推导。 */
|
|
112
|
+
timestamp?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// 公开类型
|
|
117
|
+
// ============================================================
|
|
118
|
+
|
|
119
|
+
/** custom entry 的 customType 标识(session-runner 写 / reconstructor 读,约定常量)。 */
|
|
120
|
+
export const IDENTITY_CUSTOM_TYPE = "subagent-identity";
|
|
121
|
+
|
|
122
|
+
/** 重建产出的完整 SubagentRecord 数据(身份 + 可变状态 + 派生 eventLog)。 */
|
|
123
|
+
export interface ReconstructedRecord {
|
|
124
|
+
// ── 身份(来自 custom entry)──
|
|
125
|
+
id: string;
|
|
126
|
+
agent: string;
|
|
127
|
+
mode: ExecutionMode;
|
|
128
|
+
task: string;
|
|
129
|
+
startedAt: number;
|
|
130
|
+
/** 根 Pi session ID(session 隔离过滤用)。旧文件可能缺失。 */
|
|
131
|
+
rootSessionId: string | undefined;
|
|
132
|
+
/** 直接父 subagent record ID。旧文件缺失 → undefined(顶层)。 */
|
|
133
|
+
parentRecordId: string | undefined;
|
|
134
|
+
/** subagent 递归深度。旧文件缺失 → 0(顶层)。 */
|
|
135
|
+
depth: number;
|
|
136
|
+
/** [MF#4] 本 session 的 fork 深度(来自 identity custom entry;旧文件为 undefined)。 */
|
|
137
|
+
forkDepth: number | undefined;
|
|
138
|
+
sessionFile: string;
|
|
139
|
+
// ── 可变状态(来自 message entries)──
|
|
140
|
+
status: ExecutionStatus;
|
|
141
|
+
turns: Turn[];
|
|
142
|
+
turnCount: number;
|
|
143
|
+
totalTokens: number;
|
|
144
|
+
lastError: string | undefined;
|
|
145
|
+
model: string;
|
|
146
|
+
thinkingLevel: string | undefined;
|
|
147
|
+
/** 最后一条 entry 的时间戳(ms)。供 finalize/crashed 重建填 endedAt,避免耗时无限增长。 */
|
|
148
|
+
endedAt: number | undefined;
|
|
149
|
+
/** 各 turn text 拼接的完整正文(镜像 getFullText)。 */
|
|
150
|
+
result: string | undefined;
|
|
151
|
+
/** 来自最后一条 error/aborted assistant message(无则 undefined)。 */
|
|
152
|
+
error: string | undefined;
|
|
153
|
+
/** 从 turns[] 派生的离散语义事件序列(与活态 record 同形)。 */
|
|
154
|
+
eventLog: AgentEventLogEntry[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================================
|
|
158
|
+
// 内部 helper
|
|
159
|
+
// ============================================================
|
|
160
|
+
|
|
161
|
+
/** turn_end 摘要截断长度(镜像 execution-record.ts TURN_SUMMARY_MAX)。 */
|
|
162
|
+
const TURN_SUMMARY_MAX = 80;
|
|
163
|
+
|
|
164
|
+
/** 从 turns[] 派生 eventLog(镜像 execution-record.ts getEventLog,但接受最小结构)。 */
|
|
165
|
+
function deriveEventLog(
|
|
166
|
+
turns: Turn[],
|
|
167
|
+
lastError: string | undefined,
|
|
168
|
+
startedAt: number,
|
|
169
|
+
): AgentEventLogEntry[] {
|
|
170
|
+
const log: AgentEventLogEntry[] = [];
|
|
171
|
+
for (const turn of turns) {
|
|
172
|
+
for (const tc of turn.toolCalls) {
|
|
173
|
+
const label = extractLabelFromArgs(tc.toolName, tc.args);
|
|
174
|
+
const ts = tc.startedTs;
|
|
175
|
+
log.push({ type: "tool_start", label, ts, status: "running" });
|
|
176
|
+
if (tc._status !== "running") {
|
|
177
|
+
log.push({ type: "tool_end", label, ts, status: tc._status });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (turn.closed) {
|
|
181
|
+
const summary = turn.text.length > 0
|
|
182
|
+
? (turn.text.length > TURN_SUMMARY_MAX ? turn.text.slice(0, TURN_SUMMARY_MAX) : turn.text)
|
|
183
|
+
: "turn";
|
|
184
|
+
log.push({ type: "turn_end", label: summary, ts: turn.closedTs ?? startedAt });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (lastError) {
|
|
188
|
+
log.push({ type: "error", label: lastError, ts: Date.now() });
|
|
189
|
+
}
|
|
190
|
+
return log;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 空 turn(镜像 execution-record.ts 的 emptyTurn,但本模块独立——避免循环依赖)。 */
|
|
194
|
+
function emptyTurn(): Turn {
|
|
195
|
+
return { text: "", thinking: "", toolCalls: [], usageDelta: undefined, closed: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** JsonlUsage → AgentUsage(cost 取 cost.total,与 session-runner 扁平化一致)。 */
|
|
199
|
+
function toAgentUsage(u: JsonlUsage): AgentUsage {
|
|
200
|
+
return {
|
|
201
|
+
input: u.input ?? 0,
|
|
202
|
+
output: u.output ?? 0,
|
|
203
|
+
cacheRead: u.cacheRead ?? 0,
|
|
204
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
205
|
+
cost: u.cost?.total,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 累加 AgentUsage(field-wise,镜像 execution-record.ts addUsage)。 */
|
|
210
|
+
function addUsage(prev: AgentUsage | undefined, next: AgentUsage): AgentUsage {
|
|
211
|
+
if (prev === undefined) return { ...next };
|
|
212
|
+
return {
|
|
213
|
+
input: prev.input + next.input,
|
|
214
|
+
output: prev.output + next.output,
|
|
215
|
+
cacheRead: prev.cacheRead + next.cacheRead,
|
|
216
|
+
cacheWrite: prev.cacheWrite + next.cacheWrite,
|
|
217
|
+
cost: (prev.cost ?? 0) + (next.cost ?? 0),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 校验 custom entry data 是否为合法 SubagentIdentityData。 */
|
|
222
|
+
function isIdentityData(data: unknown): data is SubagentIdentityData {
|
|
223
|
+
if (typeof data !== "object" || data === null) return false;
|
|
224
|
+
const d = data as Record<string, unknown>;
|
|
225
|
+
return (
|
|
226
|
+
typeof d.id === "string" &&
|
|
227
|
+
typeof d.agent === "string" &&
|
|
228
|
+
(d.mode === "sync" || d.mode === "background") &&
|
|
229
|
+
typeof d.task === "string" &&
|
|
230
|
+
typeof d.startedAt === "number"
|
|
231
|
+
);
|
|
232
|
+
}/**
|
|
233
|
+
* 待匹配的 toolCall(assistant 发起,等 toolResult 回填)。
|
|
234
|
+
* 记录在 assistant Turn 上,toolResult 到达时按 toolCallId 找到并填充。
|
|
235
|
+
*/
|
|
236
|
+
interface PendingToolCall {
|
|
237
|
+
toolCallId: string;
|
|
238
|
+
toolName: string;
|
|
239
|
+
args: unknown;
|
|
240
|
+
/** 所属 Turn(toolResult 回填时推进这个 Turn 的 toolCalls)。 */
|
|
241
|
+
turn: Turn;
|
|
242
|
+
/** assistant message timestamp(作为 InternalToolCall.startedTs)。 */
|
|
243
|
+
startedTs: number;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================
|
|
247
|
+
// 公开函数
|
|
248
|
+
// ============================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 从 session.jsonl 重建完整 SubagentRecord 数据。
|
|
252
|
+
*
|
|
253
|
+
* ╔══════════════════════════════════════════════════════════════════╗
|
|
254
|
+
* ║ 1. readFileSync(sessionFile) + 逐行 JSON.parse(跳 header) ║
|
|
255
|
+
* ║ (损坏行静默跳过;失败 → undefined) ║
|
|
256
|
+
* ║ 2. 扫 custom entry(customType:"subagent-identity")→ 身份 ║
|
|
257
|
+
* ║ 缺 identity → undefined(无法构造 record) ║
|
|
258
|
+
* ║ 3. 顺序遍历 message entries: ║
|
|
259
|
+
* ║ - role:"assistant" → 开新 Turn ║
|
|
260
|
+
* ║ text/thinking 累积;usage → usageDelta ║
|
|
261
|
+
* ║ toolCall block → 待匹配队列;stopReason error → lastError ║
|
|
262
|
+
* ║ - role:"toolResult" → 配对 toolCallId ║
|
|
263
|
+
* ║ 产 InternalToolCall(done/failed)推进请求 Turn ║
|
|
264
|
+
* ║ 4. 闭合所有 turn;算 turnCount/totalTokens/result/eventLog ║
|
|
265
|
+
* ╚══════════════════════════════════════════════════════════════════╝
|
|
266
|
+
*
|
|
267
|
+
* 返回 undefined:文件缺失/空/损坏/缺 identity entry/无 assistant message。
|
|
268
|
+
* 不抛——消费方(collectRecords)降级跳过该 record。
|
|
269
|
+
*
|
|
270
|
+
* status 由最后一条 assistant message 的 stopReason 推导(error/aborted → failed,
|
|
271
|
+
* 其余 → done)。cancelled 由 tombstone override(record-store 层),本函数不感知。
|
|
272
|
+
*/
|
|
273
|
+
export function reconstructFromFile(sessionFile: string): ReconstructedRecord | undefined {
|
|
274
|
+
// 读文件 + 逐行 JSON.parse(session.jsonl = newline-delimited JSON)。
|
|
275
|
+
// 第 1 行是 session header(跳过);后续每行一个 entry。
|
|
276
|
+
// 损坏行静默跳过(与 SDK parseSessionEntries 行为一致)。
|
|
277
|
+
let raw: string;
|
|
278
|
+
try {
|
|
279
|
+
raw = fs.readFileSync(sessionFile, "utf-8");
|
|
280
|
+
} catch {
|
|
281
|
+
return undefined; // 文件缺失 → 降级。
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const entries: JsonlEntry[] = [];
|
|
285
|
+
const lines = raw.split("\n");
|
|
286
|
+
for (let i = 0; i < lines.length; i++) {
|
|
287
|
+
const line = lines[i]?.trim();
|
|
288
|
+
if (!line) continue;
|
|
289
|
+
if (i === 0) {
|
|
290
|
+
// 第 1 行是 session header(type:"session"),跳过。
|
|
291
|
+
try {
|
|
292
|
+
const head = JSON.parse(line) as JsonlEntry;
|
|
293
|
+
if (head.type === "session") continue;
|
|
294
|
+
} catch (_e) {
|
|
295
|
+
void _e; // 首行损坏 → 跳过,继续解析后续。
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(line) as JsonlEntry;
|
|
301
|
+
if (parsed && typeof parsed.type === "string") {
|
|
302
|
+
entries.push(parsed);
|
|
303
|
+
}
|
|
304
|
+
} catch (_e) {
|
|
305
|
+
void _e; // 损坏行跳过(不阻断后续行)。
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (entries.length === 0) return undefined;
|
|
310
|
+
|
|
311
|
+
// ── 扫身份 custom entry(必须存在,否则无法构造 record)──
|
|
312
|
+
let identity: SubagentIdentityData | undefined;
|
|
313
|
+
let model = "";
|
|
314
|
+
let thinkingLevel: string | undefined;
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
if (entry.type === "custom" && entry.customType === IDENTITY_CUSTOM_TYPE) {
|
|
317
|
+
if (isIdentityData(entry.data)) identity = entry.data;
|
|
318
|
+
} else if (entry.type === "model_change") {
|
|
319
|
+
// model_change: {provider, modelId} → "provider/modelId"(与 record.model 同形)。
|
|
320
|
+
if (typeof entry.provider === "string" && typeof entry.modelId === "string") {
|
|
321
|
+
model = `${entry.provider}/${entry.modelId}`;
|
|
322
|
+
}
|
|
323
|
+
} else if (entry.type === "thinking_level_change") {
|
|
324
|
+
if (typeof entry.thinkingLevel === "string") thinkingLevel = entry.thinkingLevel;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (!identity) return undefined; // 缺身份 → 无法构造 record。
|
|
328
|
+
|
|
329
|
+
// ── 重建 turns[] ──
|
|
330
|
+
const turns: Turn[] = [];
|
|
331
|
+
const pending: PendingToolCall[] = [];
|
|
332
|
+
let lastError: string | undefined;
|
|
333
|
+
let totalTokens = 0;
|
|
334
|
+
/** 最后一条 assistant message 的 stopReason(推导终态 status)。 */
|
|
335
|
+
let lastStopReason: string | undefined;
|
|
336
|
+
/** 最后一条 entry 的时间戳(ms),推导 endedAt(避免重建 record 耗时随墙钟无限增长)。 */
|
|
337
|
+
let lastEntryTsMs: number | undefined;
|
|
338
|
+
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
// entry 级别 timestamp(ISO)→ ms。优先用 entry 级别(每条都有),message.timestamp 作兼底。
|
|
341
|
+
if (typeof entry.timestamp === "string") {
|
|
342
|
+
const ms = Date.parse(entry.timestamp);
|
|
343
|
+
if (!Number.isNaN(ms)) lastEntryTsMs = ms;
|
|
344
|
+
}
|
|
345
|
+
if (entry.type !== "message") continue;
|
|
346
|
+
const msg = entry.message;
|
|
347
|
+
if (!msg) continue;
|
|
348
|
+
if (typeof msg.timestamp === "number") {
|
|
349
|
+
lastEntryTsMs = msg.timestamp;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (msg.role === "assistant") {
|
|
353
|
+
const turn = emptyTurn();
|
|
354
|
+
turns.push(turn);
|
|
355
|
+
|
|
356
|
+
for (const block of msg.content) {
|
|
357
|
+
if (block.type === "text") {
|
|
358
|
+
turn.text += block.text;
|
|
359
|
+
} else if (block.type === "thinking") {
|
|
360
|
+
turn.thinking += block.thinking;
|
|
361
|
+
} else if (block.type === "toolCall") {
|
|
362
|
+
pending.push({
|
|
363
|
+
toolCallId: block.id,
|
|
364
|
+
toolName: block.name,
|
|
365
|
+
args: block.arguments,
|
|
366
|
+
turn,
|
|
367
|
+
startedTs: msg.timestamp,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (msg.usage) {
|
|
373
|
+
const u = toAgentUsage(msg.usage);
|
|
374
|
+
turn.usageDelta = addUsage(turn.usageDelta, u);
|
|
375
|
+
totalTokens += u.input + u.output + u.cacheRead + u.cacheWrite;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// stopReason 驱动 lastError(与 updateFromEvent 一致):
|
|
379
|
+
// error/aborted → 设 lastError;stop(正常结束)→ 清 lastError(镜像 turn_end 的清除语义,
|
|
380
|
+
// 即前序 turn 的瞬态 error 在后续成功 turn 后恢复,不误判 success=false)。
|
|
381
|
+
lastStopReason = msg.stopReason;
|
|
382
|
+
if (msg.stopReason === "error" || msg.stopReason === "aborted") {
|
|
383
|
+
lastError = msg.errorMessage ?? msg.stopReason;
|
|
384
|
+
} else if (msg.stopReason === "stop") {
|
|
385
|
+
lastError = undefined;
|
|
386
|
+
}
|
|
387
|
+
} else if (msg.role === "toolResult") {
|
|
388
|
+
const idx = pending.findIndex((p) => p.toolCallId === msg.toolCallId);
|
|
389
|
+
if (idx >= 0) {
|
|
390
|
+
const p = pending[idx];
|
|
391
|
+
pending.splice(idx, 1);
|
|
392
|
+
const tc: InternalToolCall = {
|
|
393
|
+
toolName: p.toolName,
|
|
394
|
+
args: p.args,
|
|
395
|
+
result: { content: msg.content, details: msg.details },
|
|
396
|
+
isError: msg.isError ?? false,
|
|
397
|
+
_status: msg.isError ? "failed" : "done",
|
|
398
|
+
startedTs: p.startedTs,
|
|
399
|
+
};
|
|
400
|
+
p.turn.toolCalls.push(tc);
|
|
401
|
+
}
|
|
402
|
+
// 未配对(孤儿 toolResult)→ 丢弃。
|
|
403
|
+
}
|
|
404
|
+
// role:"user" → 跳过(task 来自 identity custom entry)。
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (turns.length === 0) return undefined; // 无 assistant message → 空壳,降级。
|
|
408
|
+
|
|
409
|
+
// 闭合所有 turn(重建的是历史,全是已完成 turn)。
|
|
410
|
+
for (const turn of turns) {
|
|
411
|
+
turn.closed = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const turnCount = turns.length;
|
|
415
|
+
const resultText = turns
|
|
416
|
+
.map((t) => t.text)
|
|
417
|
+
.filter((t) => t.length > 0)
|
|
418
|
+
.join("\n\n");
|
|
419
|
+
|
|
420
|
+
// eventLog 从 turns[] 派生(与活态 record 的 getEventLog 同形,消费方无感知差异)。
|
|
421
|
+
const eventLog = deriveEventLog(turns, lastError, identity.startedAt);
|
|
422
|
+
|
|
423
|
+
// 终态 status:最后一条 assistant message 的 stopReason 推导(与 finalizeRecord 的判定一致)。
|
|
424
|
+
// error/aborted → failed;其余(stop/toolUse/length)→ done。
|
|
425
|
+
// cancelled 由 tombstone override(record-store 层),本函数不感知。
|
|
426
|
+
const status: ExecutionStatus =
|
|
427
|
+
lastStopReason === "error" || lastStopReason === "aborted" ? "failed" : "done";
|
|
428
|
+
|
|
429
|
+
// rootSessionId 归一化:新文件读 rootSessionId,旧文件 fallback parentSessionId。
|
|
430
|
+
const rootSessionId = identity.rootSessionId ?? identity.parentSessionId;
|
|
431
|
+
return {
|
|
432
|
+
...identity,
|
|
433
|
+
rootSessionId,
|
|
434
|
+
parentRecordId: identity.parentRecordId,
|
|
435
|
+
depth: identity.depth ?? 0,
|
|
436
|
+
forkDepth: identity.forkDepth,
|
|
437
|
+
sessionFile,
|
|
438
|
+
status,
|
|
439
|
+
turns,
|
|
440
|
+
turnCount,
|
|
441
|
+
totalTokens,
|
|
442
|
+
lastError,
|
|
443
|
+
model,
|
|
444
|
+
thinkingLevel,
|
|
445
|
+
endedAt: lastEntryTsMs,
|
|
446
|
+
result: resultText.length > 0 ? resultText : undefined,
|
|
447
|
+
error: lastError,
|
|
448
|
+
eventLog,
|
|
449
|
+
};
|
|
450
|
+
}
|