@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,778 @@
|
|
|
1
|
+
// src/core/execution-record.ts
|
|
2
|
+
//
|
|
3
|
+
// 唯一执行状态对象 + 唯一创建/更新/完成/投影入口。
|
|
4
|
+
//
|
|
5
|
+
// 收口设计(2026-06-22 重构):
|
|
6
|
+
// 一次执行的完整内容(text/thinking/toolCalls/usage)按 turn 收口在 record.turns[]。
|
|
7
|
+
// eventLog / currentActivity / result 文本均从 turns[] 派生(getEventLog /
|
|
8
|
+
// getCurrentActivity / getFullText),不再独立存储切片或缓冲。
|
|
9
|
+
//
|
|
10
|
+
// createRecord 唯一创建入口(model 创建时必填,消灭 poll 路径 model 丢失)
|
|
11
|
+
// updateFromEvent 唯一事件更新入口(累积进 turns[],消灭闭包旁路累积器)
|
|
12
|
+
// completeRecord 唯一完成入口(冻结状态)
|
|
13
|
+
// project/snapshot 唯一投影入口(两路径字段一致)
|
|
14
|
+
//
|
|
15
|
+
// Core 层叶子原语:仅依赖 types.ts。零 Pi / Runtime / TUI 依赖。
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AgentEvent,
|
|
19
|
+
AgentEventLogEntry,
|
|
20
|
+
AgentResult,
|
|
21
|
+
AgentUsage,
|
|
22
|
+
AgentUsageTotal,
|
|
23
|
+
DisplayItem,
|
|
24
|
+
ExecutionMode,
|
|
25
|
+
ExecutionRecord,
|
|
26
|
+
ExecutionStatus,
|
|
27
|
+
InternalToolCall,
|
|
28
|
+
RecordSnapshot,
|
|
29
|
+
SubagentToolDetails,
|
|
30
|
+
ToolCall,
|
|
31
|
+
ToolCallResult,
|
|
32
|
+
Turn,
|
|
33
|
+
} from "./types.ts";
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// 常量
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
/** currentActivity label 的前缀截断长度(与旧 ACTIVITY_LABEL_MAX 对齐)。 */
|
|
40
|
+
const ACTIVITY_LABEL_MAX = 60;
|
|
41
|
+
/** turn_end 派生条目 label 的最大长度(取本 turn 文本开头)。 */
|
|
42
|
+
const TURN_SUMMARY_MAX = 80;
|
|
43
|
+
/** tool label 的最大长度(command/query/url/basename 截断,保持 TUI 列宽稳定)。 */
|
|
44
|
+
const TOOL_LABEL_MAX = 100;
|
|
45
|
+
/** ms → s 换算。elapsedSeconds 唯一计算点用。 */
|
|
46
|
+
const MS_PER_SECOND = 1000;
|
|
47
|
+
|
|
48
|
+
// ============================================================
|
|
49
|
+
// Label 提取(eventLog 派生的伴生逻辑,co-locate 于 Core)
|
|
50
|
+
// ============================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 从 toolName + args 提取 eventLog label(人类可读)。
|
|
54
|
+
*
|
|
55
|
+
* read/edit/write → "{tool} {basename}"(取 path 参数)
|
|
56
|
+
* bash → "{tool} {command 首行}"(截断)
|
|
57
|
+
* web_search → "{tool} {query}"
|
|
58
|
+
* web_fetch → "{tool} {url}"
|
|
59
|
+
* 其他 / 无 args → 裸 toolName
|
|
60
|
+
*
|
|
61
|
+
* 纯函数(零依赖),由 getEventLog 派生 tool 条目时调用。
|
|
62
|
+
* 所有取自参数的字符串都经 truncateLabel 截断到 TOOL_LABEL_MAX——
|
|
63
|
+
* 保持 TUI 列宽稳定(避免一条 10KB bash 命令撑爆 compact view)。
|
|
64
|
+
*/
|
|
65
|
+
export function extractLabelFromArgs(toolName: string, args: unknown): string {
|
|
66
|
+
if (typeof args !== "object" || args === null) return toolName;
|
|
67
|
+
const a = args as Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
// 读/写/编辑类:取路径 basename(~/.pi/.../foo.ts → foo.ts)
|
|
70
|
+
// 兼容 Pi tool 的多种路径参数名:path / file_path / filePath
|
|
71
|
+
const pathLike = (a.path ?? a.file_path ?? a.filePath) as unknown;
|
|
72
|
+
if (typeof pathLike === "string" && pathLike.length > 0) {
|
|
73
|
+
const base = pathLike.split(/[\\/]/).pop() ?? pathLike;
|
|
74
|
+
return `${toolName} ${truncateLabel(base)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// bash:command 首行(截断)
|
|
78
|
+
const cmd = a.command as unknown;
|
|
79
|
+
if (typeof cmd === "string" && cmd.length > 0) {
|
|
80
|
+
const firstLine = cmd.split("\n", 1)[0].trim();
|
|
81
|
+
return `${toolName} ${truncateLabel(firstLine)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// web_search:query
|
|
85
|
+
const query = a.query as unknown;
|
|
86
|
+
if (typeof query === "string" && query.length > 0) {
|
|
87
|
+
return `${toolName} ${truncateLabel(query)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// web_fetch:url
|
|
91
|
+
const url = a.url as unknown;
|
|
92
|
+
if (typeof url === "string" && url.length > 0) {
|
|
93
|
+
return `${toolName} ${truncateLabel(url)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return toolName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 截断 label 到 maxLen(非省略号——保持列宽稳定,避免长命令/路径撑爆 TUI 列宽)。 */
|
|
100
|
+
function truncateLabel(label: string): string {
|
|
101
|
+
return label.length > TOOL_LABEL_MAX ? label.slice(0, TOOL_LABEL_MAX) : label;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 累加两个 AgentUsage(field-wise)。prev 为空时返回 next 的拷贝。
|
|
106
|
+
* 供 message_end 把 usage 增量并入 turn.usageDelta。
|
|
107
|
+
*/
|
|
108
|
+
function addUsage(prev: AgentUsage | undefined, next: AgentUsage): AgentUsage {
|
|
109
|
+
if (prev === undefined) {
|
|
110
|
+
return {
|
|
111
|
+
input: next.input ?? 0,
|
|
112
|
+
output: next.output ?? 0,
|
|
113
|
+
cacheRead: next.cacheRead ?? 0,
|
|
114
|
+
cacheWrite: next.cacheWrite ?? 0,
|
|
115
|
+
cost: next.cost,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
input: (prev.input ?? 0) + (next.input ?? 0),
|
|
120
|
+
output: (prev.output ?? 0) + (next.output ?? 0),
|
|
121
|
+
cacheRead: (prev.cacheRead ?? 0) + (next.cacheRead ?? 0),
|
|
122
|
+
cacheWrite: (prev.cacheWrite ?? 0) + (next.cacheWrite ?? 0),
|
|
123
|
+
cost: (prev.cost ?? 0) + (next.cost ?? 0),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================
|
|
128
|
+
// 创建(唯一入口)
|
|
129
|
+
// ============================================================
|
|
130
|
+
|
|
131
|
+
/** 创建一个空 turn(text/thinking 空,无 toolCalls,未闭合)。 */
|
|
132
|
+
function emptyTurn(): Turn {
|
|
133
|
+
return { text: "", thinking: "", toolCalls: [], usageDelta: undefined, closed: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 唯一创建入口。identity 字段(agent/model/thinkingLevel/mode/task)一次确定不可变。
|
|
138
|
+
*
|
|
139
|
+
* model 创建时必填——这是 poll 路径 model 丢失的架构修复
|
|
140
|
+
* (旧实现 background record 运行时丢 model,poll 返回缺字段)。
|
|
141
|
+
*/
|
|
142
|
+
export function createRecord(
|
|
143
|
+
id: string,
|
|
144
|
+
identity: {
|
|
145
|
+
agent: string;
|
|
146
|
+
model: string;
|
|
147
|
+
thinkingLevel?: string;
|
|
148
|
+
mode: ExecutionMode;
|
|
149
|
+
task: string;
|
|
150
|
+
startedAt: number;
|
|
151
|
+
/** 根 Pi session ID(session 隔离过滤用)。递归链上所有层同值。 */
|
|
152
|
+
rootSessionId?: string;
|
|
153
|
+
/** 直接父 subagent record ID。顶层为 undefined。 */
|
|
154
|
+
parentRecordId?: string;
|
|
155
|
+
/** subagent 递归深度。顶层=0。 */
|
|
156
|
+
depth?: number;
|
|
157
|
+
controller?: AbortController;
|
|
158
|
+
},
|
|
159
|
+
): ExecutionRecord {
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
agent: identity.agent,
|
|
163
|
+
model: identity.model,
|
|
164
|
+
thinkingLevel: identity.thinkingLevel,
|
|
165
|
+
mode: identity.mode,
|
|
166
|
+
task: identity.task,
|
|
167
|
+
startedAt: identity.startedAt,
|
|
168
|
+
rootSessionId: identity.rootSessionId,
|
|
169
|
+
parentRecordId: identity.parentRecordId,
|
|
170
|
+
depth: identity.depth ?? 0,
|
|
171
|
+
|
|
172
|
+
// 状态(实时更新)
|
|
173
|
+
status: "running",
|
|
174
|
+
// turns[] 初始化为 [空 turn]——第一个 turn 从创建即存在,
|
|
175
|
+
// updateFromEvent 直接往 turns[last] 累积,无需「无 turn」分支判断。
|
|
176
|
+
turns: [emptyTurn()],
|
|
177
|
+
turnCount: 0,
|
|
178
|
+
totalTokens: 0,
|
|
179
|
+
lastError: undefined,
|
|
180
|
+
|
|
181
|
+
// 完成(completeRecord 唯一写点)
|
|
182
|
+
endedAt: undefined,
|
|
183
|
+
result: undefined,
|
|
184
|
+
error: undefined,
|
|
185
|
+
agentResult: undefined,
|
|
186
|
+
|
|
187
|
+
// 控制(仅 background 持有 controller;sync 为 undefined)
|
|
188
|
+
controller: identity.controller,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================
|
|
193
|
+
// 事件更新(唯一更新点)
|
|
194
|
+
// ============================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 取当前正在进行(未 closed)的 turn;若全部 closed 则开新 turn。
|
|
198
|
+
* 保证调用后返回的 turn 一定 closed===false,可安全累积内容。
|
|
199
|
+
*/
|
|
200
|
+
function currentTurn(record: ExecutionRecord): Turn {
|
|
201
|
+
const last = record.turns[record.turns.length - 1];
|
|
202
|
+
if (last !== undefined && !last.closed) return last;
|
|
203
|
+
const fresh = emptyTurn();
|
|
204
|
+
record.turns.push(fresh);
|
|
205
|
+
return fresh;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 在 record.turns[] 范围内倒序找最后一个同名且仍 running 的 toolCall。
|
|
210
|
+
*
|
|
211
|
+
* 扫描所有 turn(非仅当前 turn)——SDK 在 turn_end 后仍可能补发滞后的 tool_end,
|
|
212
|
+
* 仅扫当前 turn 会漏配对、误 push 幽灵 ToolCall。跨 turn 扫描兜底滞后事件。
|
|
213
|
+
*
|
|
214
|
+
* 返回 [turn, index];未找到返回 undefined。
|
|
215
|
+
*/
|
|
216
|
+
function findRunningToolCall(
|
|
217
|
+
record: ExecutionRecord,
|
|
218
|
+
toolName: string,
|
|
219
|
+
): readonly [Turn, number] | undefined {
|
|
220
|
+
for (let t = record.turns.length - 1; t >= 0; t--) {
|
|
221
|
+
const turn = record.turns[t];
|
|
222
|
+
if (turn === undefined) continue;
|
|
223
|
+
for (let i = turn.toolCalls.length - 1; i >= 0; i--) {
|
|
224
|
+
const tc = turn.toolCalls[i];
|
|
225
|
+
if (tc?._status === "running" && tc.toolName === toolName) {
|
|
226
|
+
return [turn, i] as const;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 从 AgentEvent 更新 record。所有数据收口进 record.turns[]。
|
|
235
|
+
* - text/thinking:流式累积进 currentTurn()(完整内容,非切片)
|
|
236
|
+
* - tool_start/end:push 进 currentTurn().toolCalls(含完整 result)
|
|
237
|
+
* tool_end 跨 turn 扫描找 running 同名 toolCall(兜底滞后事件)
|
|
238
|
+
* - turn_end:闭合当前 turn,记 closedTs(真实墙钟,供 getEventLog);
|
|
239
|
+
* 正常闭合清 lastError(瞬态 error 恢复后不应误判 success=false)
|
|
240
|
+
* - message_end:usage 增量存进末 turn.usageDelta(直接写末 turn,不开新 turn);
|
|
241
|
+
* totalTokens 累加
|
|
242
|
+
* - error:存 record.lastError(getEventLog 派生 error 条目用)
|
|
243
|
+
*
|
|
244
|
+
* 唯一写点——session-runner 闭包不再旁路累积,collectResult 从 record 读。
|
|
245
|
+
*
|
|
246
|
+
* 穷尽性:switch 覆盖 AgentEvent 全部 variant;default 的 `never` 断言保证
|
|
247
|
+
* 新增 variant 时编译期报错(而非静默 no-op)。
|
|
248
|
+
*/
|
|
249
|
+
export function updateFromEvent(record: ExecutionRecord, event: AgentEvent): void {
|
|
250
|
+
switch (event.type) {
|
|
251
|
+
// ── text / thinking:流式累积进当前 turn ──
|
|
252
|
+
case "text_delta": {
|
|
253
|
+
currentTurn(record).text += event.delta;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
case "thinking_delta": {
|
|
257
|
+
currentTurn(record).thinking += event.delta;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── tool_start:push 一个 running 的 InternalToolCall(带 startedTs)──
|
|
262
|
+
case "tool_start": {
|
|
263
|
+
const tc: InternalToolCall = {
|
|
264
|
+
toolName: event.toolName,
|
|
265
|
+
args: event.args,
|
|
266
|
+
result: undefined,
|
|
267
|
+
isError: false,
|
|
268
|
+
_status: "running",
|
|
269
|
+
startedTs: Date.now(),
|
|
270
|
+
};
|
|
271
|
+
currentTurn(record).toolCalls.push(tc);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── tool_end:跨 turn 找 running 同名 toolCall,补全 result/isError/_status ──
|
|
276
|
+
case "tool_end": {
|
|
277
|
+
const matched = findRunningToolCall(record, event.toolName);
|
|
278
|
+
if (matched !== undefined) {
|
|
279
|
+
const [turn, i] = matched;
|
|
280
|
+
const tc = turn.toolCalls[i]!;
|
|
281
|
+
tc.args = event.args ?? tc.args;
|
|
282
|
+
tc.result = event.result;
|
|
283
|
+
tc.isError = event.isError ?? false;
|
|
284
|
+
tc._status = event.isError ? "failed" : "done";
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// 匹配失败(SDK 发了 tool_end 但无对应 tool_start,如外部注入的工具):
|
|
288
|
+
// 直接 push 一个已完成的 InternalToolCall,避免数据丢失。
|
|
289
|
+
currentTurn(record).toolCalls.push({
|
|
290
|
+
toolName: event.toolName,
|
|
291
|
+
args: event.args,
|
|
292
|
+
result: event.result,
|
|
293
|
+
isError: event.isError ?? false,
|
|
294
|
+
_status: event.isError ? "failed" : "done",
|
|
295
|
+
startedTs: Date.now(),
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── turn_end:闭合当前 turn,记 closedTs,turnCount++,清 lastError ──
|
|
301
|
+
case "turn_end": {
|
|
302
|
+
const turn = currentTurn(record);
|
|
303
|
+
turn.closed = true;
|
|
304
|
+
turn.closedTs = Date.now();
|
|
305
|
+
record.turnCount += 1;
|
|
306
|
+
// turn 正常闭合意味着本段执行成功——清掉运行期可能记录的瞬态 error,
|
|
307
|
+
// 避免瞬态 error 恢复后 session-runner 仍据 lastError 误判 success=false。
|
|
308
|
+
// (若 turn_end 后 message_end 报 error,会在 message_end 分支重新写回 lastError。)
|
|
309
|
+
record.lastError = undefined;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── message_end:usage 增量累加进 currentTurn().usageDelta,totalTokens 累加 ──
|
|
314
|
+
//
|
|
315
|
+
// usageDelta 按 message_end **累加**(非覆盖)——同一 turn 内若多次 message_end
|
|
316
|
+
// 到达(或 turn_end 后的滞后 message_end 落到 currentTurn 开的新 turn),
|
|
317
|
+
// 累加保证不丢 usage。getTotalUsage 扁平求和所有 turn,归属 turn 的精确性
|
|
318
|
+
// 不影响最终 total(无消费方读单 turn usage)。
|
|
319
|
+
case "message_end": {
|
|
320
|
+
if (event.usage) {
|
|
321
|
+
const turn = currentTurn(record);
|
|
322
|
+
turn.usageDelta = addUsage(turn.usageDelta, event.usage);
|
|
323
|
+
// totalTokens 累加四项之和(保留旧语义,投影直接读)
|
|
324
|
+
record.totalTokens +=
|
|
325
|
+
(event.usage.input ?? 0) + (event.usage.output ?? 0) +
|
|
326
|
+
(event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
|
|
327
|
+
}
|
|
328
|
+
// message_end 的 error(stopReason=error)也记进 lastError
|
|
329
|
+
if (event.error) {
|
|
330
|
+
record.lastError = event.error;
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── error:存 record.lastError(getEventLog 派生 error 条目)──
|
|
336
|
+
case "error": {
|
|
337
|
+
record.lastError = event.message;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── compaction:不产生数据(不变)──
|
|
342
|
+
case "compaction": {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
default: {
|
|
347
|
+
// 穷尽性检查:新增 AgentEvent variant 时编译期报错
|
|
348
|
+
const _exhaustive: never = event;
|
|
349
|
+
return _exhaustive;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================
|
|
355
|
+
// 派生视图(从 turns[] 推导,不存储)
|
|
356
|
+
// ============================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 从 turns[] 派生有序事件序列(eventLog)。
|
|
360
|
+
*
|
|
361
|
+
* 每个 turn 产出:tool_start/tool_end 对(按 toolCalls 顺序)+ turn_end。
|
|
362
|
+
* 若有 lastError,末尾追加 error 条目。
|
|
363
|
+
*
|
|
364
|
+
* [turn1{toolCalls:[A,B]}, turn2{toolCalls:[C]}] + lastError
|
|
365
|
+
* → [tool_start A, tool_end A, tool_start B, tool_end B, turn_end,
|
|
366
|
+
* tool_start C, tool_end C, turn_end, error]
|
|
367
|
+
*
|
|
368
|
+
* ts 为真实墙钟时间戳:tool 条目用 tc.startedTs,turn_end 用 turn.closedTs。
|
|
369
|
+
* (旧实现派生时 ts += 1 是合成值,无法表达真实时序——现已改为存真实时间戳。)
|
|
370
|
+
*
|
|
371
|
+
* 纯函数:每次调用重新生成,不缓存。消费方按需调(投影时用)。
|
|
372
|
+
*/
|
|
373
|
+
export function getEventLog(record: ExecutionRecord): AgentEventLogEntry[] {
|
|
374
|
+
const log: AgentEventLogEntry[] = [];
|
|
375
|
+
for (const turn of record.turns) {
|
|
376
|
+
for (const tc of turn.toolCalls) {
|
|
377
|
+
const label = extractLabelFromArgs(tc.toolName, tc.args);
|
|
378
|
+
const ts = tc.startedTs;
|
|
379
|
+
log.push({ type: "tool_start", label, ts, status: "running" });
|
|
380
|
+
if (tc._status !== "running") {
|
|
381
|
+
log.push({ type: "tool_end", label, ts, status: tc._status });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (turn.closed) {
|
|
385
|
+
const summary = turn.text.length > 0
|
|
386
|
+
? (turn.text.length > TURN_SUMMARY_MAX ? turn.text.slice(0, TURN_SUMMARY_MAX) : turn.text)
|
|
387
|
+
: "turn";
|
|
388
|
+
log.push({ type: "turn_end", label: summary, ts: turn.closedTs ?? record.startedAt });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (record.lastError) {
|
|
392
|
+
log.push({ type: "error", label: record.lastError, ts: Date.now() });
|
|
393
|
+
}
|
|
394
|
+
return log;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* [STEP3] 从 turns[] 派生 displayItems(对齐 nicobailon getDisplayItems)。
|
|
399
|
+
*
|
|
400
|
+
* 与 getEventLog 的区别:产出可渲染单元(toolCall 含完整 name+args,text 含正文),
|
|
401
|
+
* 而非离散事件。renderResult compact 用此数据 + formatToolCall 生成与 nicobailon
|
|
402
|
+
* 一致的 `→ formatToolCall` 行格式。
|
|
403
|
+
*
|
|
404
|
+
* 派生规则(与 nicobailon getDisplayItems(messages) 等价):
|
|
405
|
+
* - 遍历 turns[],每个 turn:先 text(如果有),再 toolCalls
|
|
406
|
+
* - toolCall:{ type:"toolCall", name, args, status }
|
|
407
|
+
* - text:{ type:"text", text }(取首行,避免撑爆 compact)
|
|
408
|
+
* - 跳过 thinking(与 nicobailon 一致,不在 compact 展示推理)
|
|
409
|
+
*
|
|
410
|
+
* 参数类型放宽为 `{ turns: readonly Turn[] }` 结构子集——ExecutionRecord 和
|
|
411
|
+
* ReconstructedRecord 都满足,磁盘重建路径(record-store)可复用此函数派生
|
|
412
|
+
* displayItems,而非给空数组(否则终态 record 详情看不到 text)。
|
|
413
|
+
*/
|
|
414
|
+
export function getDisplayItems(record: { turns: readonly Turn[] }): DisplayItem[] {
|
|
415
|
+
const items: DisplayItem[] = [];
|
|
416
|
+
for (const turn of record.turns) {
|
|
417
|
+
// assistant 正文(与 nicobailon 顺序一致:先 text 后 toolCall)
|
|
418
|
+
if (turn.text.length > 0) {
|
|
419
|
+
items.push({ type: "text", text: turn.text });
|
|
420
|
+
}
|
|
421
|
+
for (const tc of turn.toolCalls) {
|
|
422
|
+
items.push({
|
|
423
|
+
type: "toolCall",
|
|
424
|
+
name: tc.toolName,
|
|
425
|
+
args: (tc.args ?? {}) as Record<string, unknown>,
|
|
426
|
+
status: tc._status,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return items;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 从 turns[] 末尾推导当前活动行(running 时)。
|
|
435
|
+
*
|
|
436
|
+
* 优先级:最后一个未闭合 turn 的末尾 running toolCall → thinking → text → undefined
|
|
437
|
+
*
|
|
438
|
+
* 仅 status==="running" 时返回;terminal 态返回 undefined。
|
|
439
|
+
*
|
|
440
|
+
* 注意:返回的 type 联合("tool"|"text"|"thinking")是手写的,未通过类型守卫从
|
|
441
|
+
* AgentEvent 派生——它映射的是累积的 turn 状态(InternalToolCall._status + turn.thinking/text),
|
|
442
|
+
* 而非单个事件。若未来新增 turn 内容模式(如 reasoning_summary),须同步扩展本函数,
|
|
443
|
+
* 否则会静默返回 undefined(活动行运行中途消失)。updateFromEvent 的 switch 有 never 穷尽
|
|
444
|
+
* 检查,但本函数没有,依赖人工同步。
|
|
445
|
+
*/
|
|
446
|
+
export function getCurrentActivity(
|
|
447
|
+
record: ExecutionRecord,
|
|
448
|
+
): { type: "tool" | "text" | "thinking"; label: string } | undefined {
|
|
449
|
+
if (record.status !== "running") return undefined;
|
|
450
|
+
const turn = record.turns[record.turns.length - 1];
|
|
451
|
+
if (turn === undefined || turn.closed) return undefined;
|
|
452
|
+
|
|
453
|
+
// 1. 倒序找最后一个 running 的 toolCall
|
|
454
|
+
for (let i = turn.toolCalls.length - 1; i >= 0; i--) {
|
|
455
|
+
const tc = turn.toolCalls[i];
|
|
456
|
+
if (tc?._status === "running") {
|
|
457
|
+
return { type: "tool", label: extractLabelFromArgs(tc.toolName, tc.args) };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// 2. 正在 thinking
|
|
461
|
+
if (turn.thinking) {
|
|
462
|
+
return { type: "thinking", label: turn.thinking.slice(0, ACTIVITY_LABEL_MAX) };
|
|
463
|
+
}
|
|
464
|
+
// 3. 正在输出 text
|
|
465
|
+
if (turn.text) {
|
|
466
|
+
return { type: "text", label: turn.text.slice(0, ACTIVITY_LABEL_MAX) };
|
|
467
|
+
}
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 聚合所有 turn 的 text 为完整文本(替代旧 collectResponseText)。
|
|
473
|
+
*
|
|
474
|
+
* 单一数据源:不再读 session.messages,text 完全来自 record.turns[] 的流式累积。
|
|
475
|
+
* 多 turn 用空行分隔(每个 turn 是一段独立的 assistant 输出)。
|
|
476
|
+
*
|
|
477
|
+
* 语义对齐旧 collectResponseText:后者只取最后一条 assistant message 的 text。
|
|
478
|
+
* turns[] 收口后,每条 assistant message 对应一个 turn,故 join 所有非空 turn 文本
|
|
479
|
+
* 与「拼接所有 assistant message」语义一致。单 turn 场景两者完全等价。
|
|
480
|
+
*/
|
|
481
|
+
export function getFullText(record: ExecutionRecord): string {
|
|
482
|
+
return record.turns
|
|
483
|
+
.map((t) => t.text)
|
|
484
|
+
.filter((text) => text.length > 0)
|
|
485
|
+
.join("\n\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 聚合所有 turn 的 toolCalls(扁平化),并 strip InternalToolCall 的内部字段。
|
|
490
|
+
* 供 collectResult / schema enforcement 读,替代旧闭包 toolCalls 旁路。
|
|
491
|
+
*
|
|
492
|
+
* 返回 ToolCall[](不含 _status / startedTs)——跨边界导出形状清洁,
|
|
493
|
+
* 避免内部状态机字段泄漏到 AgentResult.toolCalls / 持久化层。
|
|
494
|
+
*/
|
|
495
|
+
export function getAllToolCalls(record: ExecutionRecord): ToolCall[] {
|
|
496
|
+
return record.turns.flatMap((t) => t.toolCalls.map(stripInternal));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 把 InternalToolCall 映射回纯净的 ToolCall(丢弃 _status / startedTs)。 */
|
|
500
|
+
function stripInternal(tc: InternalToolCall): ToolCall {
|
|
501
|
+
return {
|
|
502
|
+
toolName: tc.toolName,
|
|
503
|
+
args: tc.args,
|
|
504
|
+
result: tc.result,
|
|
505
|
+
isError: tc.isError,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 聚合所有 turn 的 usageDelta 为完整 usage(含 total + cost)。
|
|
511
|
+
* 全零则返回 undefined(与旧 toUsageTotal 语义一致)。
|
|
512
|
+
*
|
|
513
|
+
* cost 来自 SdkEvent.message.usage.cost.total(message_end 时透传到 usageDelta)。
|
|
514
|
+
* 旧 toUsageTotal/session-runner 累积 cost;本重构保留该行为。
|
|
515
|
+
*/
|
|
516
|
+
export function getTotalUsage(record: ExecutionRecord): AgentUsageTotal | undefined {
|
|
517
|
+
let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
|
|
518
|
+
for (const turn of record.turns) {
|
|
519
|
+
const u = turn.usageDelta;
|
|
520
|
+
if (u) {
|
|
521
|
+
input += u.input ?? 0;
|
|
522
|
+
output += u.output ?? 0;
|
|
523
|
+
cacheRead += u.cacheRead ?? 0;
|
|
524
|
+
cacheWrite += u.cacheWrite ?? 0;
|
|
525
|
+
cost += u.cost ?? 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
529
|
+
if (total === 0) return undefined;
|
|
530
|
+
return { input, output, cacheRead, cacheWrite, total, cost };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ============================================================
|
|
534
|
+
// 完成(唯一入口)
|
|
535
|
+
// ============================================================
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* status 状态机的 CAS 互斥锁。仅当 `record.status === "running"` 时改为 target
|
|
539
|
+
* 并返回 true,否则返回 false。**status 状态机本身就是互斥锁**——终态
|
|
540
|
+
* (done/failed/cancelled/crashed)不可逆,check-then-set 在 JS 单线程事件循环里天然原子。
|
|
541
|
+
*
|
|
542
|
+
* 用途:executor 的收尾竞争。cancelBackground 与 background detached 完成回调
|
|
543
|
+
* 都调 tryTransition 抢锁:抢到负责完整收尾,没抢到闭嘴不做事。
|
|
544
|
+
*
|
|
545
|
+
* target 仅限正常执行流的三终态。crashed 不作 target——它由重建路径推断
|
|
546
|
+
* (alive marker 存在但 session 文件无终态),不走 CAS;重建路径用 markReconstructedStatus
|
|
547
|
+
* 直接赋值。收窄 target 类型可在编译期拒绍误用 tryTransition(record, "crashed")。
|
|
548
|
+
*/
|
|
549
|
+
export function tryTransition(
|
|
550
|
+
record: ExecutionRecord,
|
|
551
|
+
target: "done" | "failed" | "cancelled",
|
|
552
|
+
): boolean {
|
|
553
|
+
if (record.status !== "running") return false;
|
|
554
|
+
record.status = target;
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* 重建专用收口:跳过 CAS 直接赋值 status。
|
|
560
|
+
*
|
|
561
|
+
* 仅用于 session-reconstructor 从 session.jsonl 重建终态 record 时——
|
|
562
|
+
* 重建的 record 没有 running 状态需要保护,直接赋值即可。
|
|
563
|
+
* 禁止在正常执行流程中使用此函数(应使用 tryTransition)。
|
|
564
|
+
*/
|
|
565
|
+
export function markReconstructedStatus(
|
|
566
|
+
record: { status: ExecutionStatus },
|
|
567
|
+
status: ExecutionStatus,
|
|
568
|
+
): void {
|
|
569
|
+
record.status = status;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 唯一完成入口。冻结状态(写 endedAt/agentResult/result/error)。
|
|
574
|
+
* 不修改 turns/totalTokens——已由 updateFromEvent 累积,completeRecord 只读不重置。
|
|
575
|
+
*
|
|
576
|
+
* ⚠ 前置条件:调用方必须先通过 tryTransition 抢到锁(status 已被 CAS 设为 target)。
|
|
577
|
+
*/
|
|
578
|
+
export function completeRecord(
|
|
579
|
+
record: ExecutionRecord,
|
|
580
|
+
result: AgentResult,
|
|
581
|
+
status: "done" | "failed" | "cancelled",
|
|
582
|
+
): void {
|
|
583
|
+
record.status = status;
|
|
584
|
+
record.endedAt = Date.now();
|
|
585
|
+
record.agentResult = result;
|
|
586
|
+
record.result = result.text;
|
|
587
|
+
record.error = result.error;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ============================================================
|
|
591
|
+
// 投影(唯一 → Details / Snapshot / Persisted)
|
|
592
|
+
// ============================================================
|
|
593
|
+
|
|
594
|
+
/** elapsedSeconds 唯一计算点(共享 helper,消除三处发散)。endedAt 缺失用 Date.now()。 */
|
|
595
|
+
export function computeElapsedSeconds(record: { startedAt: number; endedAt?: number }): number {
|
|
596
|
+
const end = record.endedAt ?? Date.now();
|
|
597
|
+
return Math.floor((end - record.startedAt) / MS_PER_SECOND);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 投影到 SubagentToolDetails。elapsedSeconds/currentActivity/eventLog 均现算派生。
|
|
602
|
+
*/
|
|
603
|
+
export function project(record: ExecutionRecord): SubagentToolDetails {
|
|
604
|
+
return {
|
|
605
|
+
status: record.status,
|
|
606
|
+
mode: record.mode,
|
|
607
|
+
agent: record.agent,
|
|
608
|
+
model: record.model,
|
|
609
|
+
thinkingLevel: record.thinkingLevel,
|
|
610
|
+
turns: record.turnCount,
|
|
611
|
+
totalTokens: record.totalTokens,
|
|
612
|
+
elapsedSeconds: computeElapsedSeconds(record),
|
|
613
|
+
eventLog: getEventLog(record),
|
|
614
|
+
displayItems: getDisplayItems(record),
|
|
615
|
+
result: record.result,
|
|
616
|
+
error: record.error,
|
|
617
|
+
currentActivity: getCurrentActivity(record),
|
|
618
|
+
parsedOutput: record.agentResult?.parsedOutput,
|
|
619
|
+
sessionFile: record.sessionFile,
|
|
620
|
+
patchFile: record.patchFile,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 投影到 live 进度快照。elapsedSeconds/currentActivity/eventLog 均现算派生。
|
|
626
|
+
* 供 WorkflowsView 在 agent 运行期间读取实时进度。
|
|
627
|
+
*/
|
|
628
|
+
export function projectLiveProgress(record: ExecutionRecord): {
|
|
629
|
+
status: ExecutionRecord["status"];
|
|
630
|
+
turns: number;
|
|
631
|
+
totalTokens: number;
|
|
632
|
+
elapsedSeconds: number;
|
|
633
|
+
eventLog: AgentEventLogEntry[];
|
|
634
|
+
currentActivity: ReturnType<typeof getCurrentActivity>;
|
|
635
|
+
lastError: string | undefined;
|
|
636
|
+
} {
|
|
637
|
+
return {
|
|
638
|
+
status: record.status,
|
|
639
|
+
turns: record.turnCount,
|
|
640
|
+
totalTokens: record.totalTokens,
|
|
641
|
+
elapsedSeconds: computeElapsedSeconds(record),
|
|
642
|
+
eventLog: getEventLog(record),
|
|
643
|
+
currentActivity: getCurrentActivity(record),
|
|
644
|
+
lastError: record.lastError,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 投影到只读快照(TUI list / poll 消费)。
|
|
650
|
+
* 浅拷贝 turns[],字段标 readonly 阻止 TUI 回写。
|
|
651
|
+
*/
|
|
652
|
+
export function snapshot(record: ExecutionRecord): RecordSnapshot {
|
|
653
|
+
return {
|
|
654
|
+
id: record.id,
|
|
655
|
+
agent: record.agent,
|
|
656
|
+
model: record.model,
|
|
657
|
+
thinkingLevel: record.thinkingLevel,
|
|
658
|
+
mode: record.mode,
|
|
659
|
+
task: record.task,
|
|
660
|
+
status: record.status,
|
|
661
|
+
turns: record.turnCount,
|
|
662
|
+
totalTokens: record.totalTokens,
|
|
663
|
+
startedAt: record.startedAt,
|
|
664
|
+
endedAt: record.endedAt,
|
|
665
|
+
result: record.result,
|
|
666
|
+
error: record.error,
|
|
667
|
+
sessionFile: record.sessionFile,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ============================================================
|
|
672
|
+
// JSONL → AgentEvent 翻译(从 live/jsonl-to-agent-event 迁入)
|
|
673
|
+
// ============================================================
|
|
674
|
+
|
|
675
|
+
/** subprocess JSONL 事件(JSON.parse 结果)。duck-typed,对应 SDK SdkEvent。 */
|
|
676
|
+
type JsonlEvent = Record<string, unknown>;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 把一条 JSONL 事件翻译成 AgentEvent。
|
|
680
|
+
*
|
|
681
|
+
* 返回 undefined 表示该事件不映射到任何 AgentEvent(如 session header、message_start、
|
|
682
|
+
* tool_execution_update),调用方应跳过。
|
|
683
|
+
*
|
|
684
|
+
* 一个 JSONL 事件可能产出**多条** AgentEvent(message_end 的 usage + error 各一条),
|
|
685
|
+
* 故返回数组。绝大多数情况长度为 0 或 1;message_end 最多 2 条。
|
|
686
|
+
*/
|
|
687
|
+
export function jsonlToAgentEvent(raw: JsonlEvent): AgentEvent[] {
|
|
688
|
+
const type = raw.type;
|
|
689
|
+
|
|
690
|
+
switch (type) {
|
|
691
|
+
case "session":
|
|
692
|
+
case "message_start":
|
|
693
|
+
case "turn_start":
|
|
694
|
+
case "tool_execution_update":
|
|
695
|
+
return [];
|
|
696
|
+
|
|
697
|
+
case "tool_execution_start": {
|
|
698
|
+
const toolName = typeof raw.toolName === "string" ? raw.toolName : "";
|
|
699
|
+
return [{ type: "tool_start", toolName, args: raw.args }];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
case "tool_execution_end": {
|
|
703
|
+
const toolName = typeof raw.toolName === "string" ? raw.toolName : "";
|
|
704
|
+
const isError = raw.isError === true;
|
|
705
|
+
return [{
|
|
706
|
+
type: "tool_end",
|
|
707
|
+
toolName,
|
|
708
|
+
args: raw.args,
|
|
709
|
+
result: raw.result as ToolCallResult | undefined,
|
|
710
|
+
isError,
|
|
711
|
+
}];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
case "message_update": {
|
|
715
|
+
const ame = raw.assistantMessageEvent as Record<string, unknown> | undefined;
|
|
716
|
+
if (ame?.type === "thinking_delta") {
|
|
717
|
+
const delta = typeof ame.delta === "string" ? ame.delta : "";
|
|
718
|
+
return [{ type: "thinking_delta", delta }];
|
|
719
|
+
}
|
|
720
|
+
if (ame !== undefined && ame.delta !== undefined) {
|
|
721
|
+
const delta = typeof ame.delta === "string" ? ame.delta : String(ame.delta);
|
|
722
|
+
return [{ type: "text_delta", delta }];
|
|
723
|
+
}
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
case "turn_end": {
|
|
728
|
+
return [{ type: "turn_end" }];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
case "message_end": {
|
|
732
|
+
return accumulateMessageEndForRecord(raw);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
case "compaction_start": {
|
|
736
|
+
return [{ type: "compaction" }];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
default:
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** message_end 翻译:usage 拍平 + stopReason=error/aborted 额外产 error 事件。 */
|
|
745
|
+
function accumulateMessageEndForRecord(raw: JsonlEvent): AgentEvent[] {
|
|
746
|
+
const events: AgentEvent[] = [];
|
|
747
|
+
const msg = raw.message as Record<string, unknown> | undefined;
|
|
748
|
+
const usageRaw = (typeof msg?.usage === "object" && msg.usage !== null)
|
|
749
|
+
? msg.usage as Record<string, unknown>
|
|
750
|
+
: undefined;
|
|
751
|
+
|
|
752
|
+
if (usageRaw) {
|
|
753
|
+
const costObj = (typeof usageRaw.cost === "object" && usageRaw.cost !== null)
|
|
754
|
+
? usageRaw.cost as Record<string, unknown>
|
|
755
|
+
: undefined;
|
|
756
|
+
// MF-3 fix: 显式提取字段 + Number.isFinite 守卫,不使用 spread + as 断言
|
|
757
|
+
const numOrZero = (v: unknown): number =>
|
|
758
|
+
typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
759
|
+
const usage: AgentUsage = {
|
|
760
|
+
input: numOrZero(usageRaw.input),
|
|
761
|
+
output: numOrZero(usageRaw.output),
|
|
762
|
+
cacheRead: numOrZero(usageRaw.cacheRead),
|
|
763
|
+
cacheWrite: numOrZero(usageRaw.cacheWrite),
|
|
764
|
+
cost: typeof costObj?.total === "number" ? costObj.total : undefined,
|
|
765
|
+
};
|
|
766
|
+
events.push({ type: "message_end", usage });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const stopReason = msg?.stopReason;
|
|
770
|
+
if (stopReason === "error" || stopReason === "aborted") {
|
|
771
|
+
const errorMessage = typeof msg?.errorMessage === "string"
|
|
772
|
+
? msg.errorMessage
|
|
773
|
+
: (typeof raw.reason === "string" ? raw.reason : String(stopReason));
|
|
774
|
+
events.push({ type: "error", message: errorMessage });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return events;
|
|
778
|
+
}
|