ronds_ai 0.1.21 → 0.1.22

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.
@@ -0,0 +1,803 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const MAX_PENDING_TASK_NOTIFICATIONS = 50;
5
+
6
+ /**
7
+ * 创建新的会话增量读取状态。
8
+ */
9
+ function createSessionState(value = {}) {
10
+ return {
11
+ offset: Number(value.offset || 0),
12
+ buffer: String(value.buffer || ''),
13
+ turnCount: Number(value.turnCount ?? value.turn_count ?? 0),
14
+ pendingAgentTurns: Array.isArray(value.pendingAgentTurns ?? value.pending_agent_turns)
15
+ ? (value.pendingAgentTurns ?? value.pending_agent_turns)
16
+ : [],
17
+ pendingTaskNotifications: Array.isArray(
18
+ value.pendingTaskNotifications ?? value.pending_task_notifications,
19
+ )
20
+ ? (value.pendingTaskNotifications ?? value.pending_task_notifications)
21
+ : [],
22
+ };
23
+ }
24
+
25
+ /**
26
+ * 从 transcript 行中读取消息内容。
27
+ */
28
+ function getContentFromRow(row) {
29
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
30
+ return null;
31
+ }
32
+ return row.message && typeof row.message === 'object'
33
+ ? row.message.content
34
+ : row.content;
35
+ }
36
+
37
+ /**
38
+ * 从 transcript 行中解析 user 或 assistant 角色。
39
+ */
40
+ function getRoleFromRow(row) {
41
+ if (!row || typeof row !== 'object') {
42
+ return null;
43
+ }
44
+ if (row.type === 'user' || row.type === 'assistant') {
45
+ return row.type;
46
+ }
47
+ const role = row.message && row.message.role;
48
+ return role === 'user' || role === 'assistant' ? role : null;
49
+ }
50
+
51
+ /**
52
+ * 获取 Claude 消息标识。
53
+ */
54
+ function getMessageId(row) {
55
+ const messageId = row && row.message && row.message.id;
56
+ return typeof messageId === 'string' && messageId ? messageId : null;
57
+ }
58
+
59
+ /**
60
+ * 获取 Assistant 消息使用的模型名称。
61
+ */
62
+ function getModel(row) {
63
+ const model = row && row.message && row.message.model;
64
+ return typeof model === 'string' && model ? model : 'claude';
65
+ }
66
+
67
+ /**
68
+ * 提取原插件支持的 Anthropic token usage 字段。
69
+ */
70
+ function getUsageDetailsFromRow(row) {
71
+ const usage = row && row.message && row.message.usage;
72
+ if (!usage || typeof usage !== 'object' || Array.isArray(usage)) {
73
+ return null;
74
+ }
75
+
76
+ const mapping = [
77
+ ['input_tokens', 'input'],
78
+ ['output_tokens', 'output'],
79
+ ['cache_read_input_tokens', 'cache_read_input_tokens'],
80
+ ['cache_creation_input_tokens', 'cache_creation_input_tokens'],
81
+ ];
82
+ const details = {};
83
+ for (const [source, target] of mapping) {
84
+ if (Number.isInteger(usage[source]) && usage[source] > 0) {
85
+ details[target] = usage[source];
86
+ }
87
+ }
88
+ return Object.keys(details).length > 0 ? details : null;
89
+ }
90
+
91
+ /**
92
+ * 将 transcript 时间转换为 Date。
93
+ */
94
+ function parseTimestamp(value) {
95
+ const raw = value && typeof value === 'object' ? value.timestamp : value;
96
+ if (typeof raw !== 'string' || !raw) {
97
+ return null;
98
+ }
99
+ const parsed = new Date(raw);
100
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
101
+ }
102
+
103
+ /**
104
+ * 仅提取原 Python 插件采集的 text 内容,不采集 thinking。
105
+ */
106
+ function extractTextFromContent(content) {
107
+ if (typeof content === 'string') {
108
+ return content;
109
+ }
110
+ if (!Array.isArray(content)) {
111
+ return '';
112
+ }
113
+ return content
114
+ .map((block) => {
115
+ if (typeof block === 'string') {
116
+ return block;
117
+ }
118
+ return block && block.type === 'text' ? String(block.text || '') : '';
119
+ })
120
+ .filter(Boolean)
121
+ .join('\n');
122
+ }
123
+
124
+ /**
125
+ * 截断过长文本并返回长度元数据。
126
+ */
127
+ function truncateText(value, maxChars) {
128
+ const text = value == null ? '' : String(value);
129
+ if (text.length <= maxChars) {
130
+ return {
131
+ text,
132
+ metadata: { truncated: false, orig_len: text.length },
133
+ };
134
+ }
135
+ const crypto = require('crypto');
136
+ const kept = text.slice(0, maxChars);
137
+ return {
138
+ text: kept,
139
+ metadata: {
140
+ truncated: true,
141
+ orig_len: text.length,
142
+ kept_len: kept.length,
143
+ sha256: crypto.createHash('sha256').update(text, 'utf8').digest('hex'),
144
+ },
145
+ };
146
+ }
147
+
148
+ /**
149
+ * 取得内容中的 tool_use 块。
150
+ */
151
+ function getToolUseBlocks(content) {
152
+ return Array.isArray(content)
153
+ ? content.filter((block) => block && typeof block === 'object' && block.type === 'tool_use')
154
+ : [];
155
+ }
156
+
157
+ /**
158
+ * 取得内容中的 tool_result 块。
159
+ */
160
+ function getToolResultBlocks(content) {
161
+ return Array.isArray(content)
162
+ ? content.filter((block) => block && typeof block === 'object' && block.type === 'tool_result')
163
+ : [];
164
+ }
165
+
166
+ /**
167
+ * 判断当前行是否为工具返回消息。
168
+ */
169
+ function isToolResultRow(row) {
170
+ return getRoleFromRow(row) === 'user'
171
+ && getToolResultBlocks(getContentFromRow(row)).length > 0;
172
+ }
173
+
174
+ /**
175
+ * 从上次 byte offset 开始读取新增 JSONL。
176
+ */
177
+ function readNewJsonl(transcriptPath, sessionState) {
178
+ if (!fs.existsSync(transcriptPath)) {
179
+ return { rows: [], sessionState };
180
+ }
181
+
182
+ const fileSize = fs.statSync(transcriptPath).size;
183
+ if (fileSize < sessionState.offset) {
184
+ sessionState.offset = 0;
185
+ sessionState.buffer = '';
186
+ }
187
+
188
+ const length = fileSize - sessionState.offset;
189
+ if (length <= 0) {
190
+ return { rows: [], sessionState };
191
+ }
192
+
193
+ const descriptor = fs.openSync(transcriptPath, 'r');
194
+ let chunk;
195
+ try {
196
+ chunk = Buffer.alloc(length);
197
+ fs.readSync(descriptor, chunk, 0, length, sessionState.offset);
198
+ } finally {
199
+ fs.closeSync(descriptor);
200
+ }
201
+
202
+ sessionState.offset = fileSize;
203
+ const combined = sessionState.buffer + chunk.toString('utf8');
204
+ const lines = combined.split('\n');
205
+ sessionState.buffer = lines.pop() || '';
206
+
207
+ const rows = [];
208
+ for (const rawLine of lines) {
209
+ const line = rawLine.trim();
210
+ if (!line) {
211
+ continue;
212
+ }
213
+ try {
214
+ const parsed = JSON.parse(line);
215
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
216
+ rows.push(parsed);
217
+ }
218
+ } catch {
219
+ // 与原插件一致:忽略损坏的 JSONL 行。
220
+ }
221
+ }
222
+ return { rows, sessionState };
223
+ }
224
+
225
+ /**
226
+ * 从简单 XML 标记中提取内容。
227
+ */
228
+ function extractXmlTagValue(text, tag) {
229
+ const start = `<${tag}>`;
230
+ const end = `</${tag}>`;
231
+ const startIndex = text.indexOf(start);
232
+ if (startIndex < 0) {
233
+ return null;
234
+ }
235
+ const endIndex = text.indexOf(end, startIndex + start.length);
236
+ return endIndex < 0 ? null : text.slice(startIndex + start.length, endIndex);
237
+ }
238
+
239
+ /**
240
+ * 判断是否为 Claude Code 的任务通知行。
241
+ */
242
+ function isTaskNotificationRow(row) {
243
+ if (row && row.origin && row.origin.kind === 'task-notification') {
244
+ return true;
245
+ }
246
+ return extractTextFromContent(getContentFromRow(row))
247
+ .trimStart()
248
+ .startsWith('<task-notification>');
249
+ }
250
+
251
+ /**
252
+ * 从任务通知中获取 tool_use_id。
253
+ */
254
+ function getToolUseIdFromTaskNotification(row) {
255
+ const value = extractXmlTagValue(
256
+ extractTextFromContent(getContentFromRow(row)),
257
+ 'tool-use-id',
258
+ );
259
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
260
+ }
261
+
262
+ /**
263
+ * 从任务通知中获取 task_id。
264
+ */
265
+ function getTaskIdFromTaskNotification(row) {
266
+ const value = extractXmlTagValue(
267
+ extractTextFromContent(getContentFromRow(row)),
268
+ 'task-id',
269
+ );
270
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
271
+ }
272
+
273
+ /**
274
+ * 综合通知字段和子 Agent 映射解析 tool_use_id。
275
+ */
276
+ function getToolUseIdForTaskNotification(row, taskIdToToolUseId = {}) {
277
+ if (!isTaskNotificationRow(row)) {
278
+ return null;
279
+ }
280
+ const direct = getToolUseIdFromTaskNotification(row);
281
+ if (direct) {
282
+ return direct;
283
+ }
284
+ const taskId = getTaskIdFromTaskNotification(row);
285
+ return taskId ? taskIdToToolUseId[taskId] || null : null;
286
+ }
287
+
288
+ /**
289
+ * 从任务通知中提取最终结果。
290
+ */
291
+ function getResultFromTaskNotification(row) {
292
+ const text = extractTextFromContent(getContentFromRow(row));
293
+ const result = extractXmlTagValue(text, 'result');
294
+ return result == null ? text : result;
295
+ }
296
+
297
+ /**
298
+ * 查找正在等待指定异步工具的历史 Turn。
299
+ */
300
+ function findPendingAgentTurn(sessionState, toolUseId) {
301
+ return sessionState.pendingAgentTurns.find((pendingTurn) => {
302
+ if (!pendingTurn || !Array.isArray(pendingTurn.rows)) {
303
+ return false;
304
+ }
305
+ return (Array.isArray(pendingTurn.pendingToolUseIds)
306
+ && pendingTurn.pendingToolUseIds.includes(toolUseId))
307
+ || (Array.isArray(pendingTurn.resolvedToolUseIds)
308
+ && pendingTurn.resolvedToolUseIds.includes(toolUseId));
309
+ }) || null;
310
+ }
311
+
312
+ /**
313
+ * 将异步任务通知路由到被延迟的 Turn。
314
+ */
315
+ function resolveDeferredAgentTurns(rows, sessionState, taskIdToToolUseId = {}) {
316
+ const remainingRows = [];
317
+ const stashedNotifications = [];
318
+
319
+ /**
320
+ * 将单条通知追加到匹配的延迟 Turn。
321
+ */
322
+ function routeToPendingTurn(pendingTurn, row, toolUseId) {
323
+ pendingTurn.rows.push(row);
324
+ const pendingIds = pendingTurn.pendingToolUseIds;
325
+ if (Array.isArray(pendingIds) && pendingIds.includes(toolUseId)) {
326
+ pendingIds.splice(pendingIds.indexOf(toolUseId), 1);
327
+ pendingTurn.resolvedToolUseIds = pendingTurn.resolvedToolUseIds || [];
328
+ pendingTurn.resolvedToolUseIds.push(toolUseId);
329
+ }
330
+ }
331
+
332
+ const priorNotifications = sessionState.pendingTaskNotifications;
333
+ for (const row of priorNotifications) {
334
+ const toolUseId = getToolUseIdForTaskNotification(row, taskIdToToolUseId);
335
+ if (!toolUseId) {
336
+ stashedNotifications.push(row);
337
+ continue;
338
+ }
339
+ const pendingTurn = findPendingAgentTurn(sessionState, toolUseId);
340
+ if (pendingTurn) {
341
+ routeToPendingTurn(pendingTurn, row, toolUseId);
342
+ }
343
+ }
344
+
345
+ for (const row of rows) {
346
+ if (!isTaskNotificationRow(row)) {
347
+ remainingRows.push(row);
348
+ continue;
349
+ }
350
+ const toolUseId = getToolUseIdForTaskNotification(row, taskIdToToolUseId);
351
+ if (!toolUseId) {
352
+ stashedNotifications.push(row);
353
+ continue;
354
+ }
355
+ const pendingTurn = findPendingAgentTurn(sessionState, toolUseId);
356
+ if (!pendingTurn) {
357
+ remainingRows.push(row);
358
+ continue;
359
+ }
360
+ routeToPendingTurn(pendingTurn, row, toolUseId);
361
+ }
362
+
363
+ sessionState.pendingTaskNotifications = stashedNotifications
364
+ .slice(-MAX_PENDING_TASK_NOTIFICATIONS);
365
+ const resolvedTurnRows = [];
366
+ sessionState.pendingAgentTurns = sessionState.pendingAgentTurns.filter((pendingTurn) => {
367
+ if (!pendingTurn || !Array.isArray(pendingTurn.rows)) {
368
+ return false;
369
+ }
370
+ if (Array.isArray(pendingTurn.pendingToolUseIds)
371
+ && pendingTurn.pendingToolUseIds.length > 0) {
372
+ return true;
373
+ }
374
+ resolvedTurnRows.push(pendingTurn.rows);
375
+ return false;
376
+ });
377
+ return { resolvedTurnRows, remainingRows };
378
+ }
379
+
380
+ /**
381
+ * 取出所有仍未完成的异步 Agent Turn,供 SessionEnd 收尾。
382
+ */
383
+ function popAllDeferredAgentTurnRows(sessionState) {
384
+ const rowLists = sessionState.pendingAgentTurns
385
+ .filter((entry) => entry && Array.isArray(entry.rows) && entry.rows.length > 0)
386
+ .map((entry) => entry.rows);
387
+ sessionState.pendingAgentTurns = [];
388
+ return rowLists;
389
+ }
390
+
391
+ /**
392
+ * 读取 tool_result 的文本形式。
393
+ */
394
+ function getToolResultText(toolResultEntry) {
395
+ if (!toolResultEntry || typeof toolResultEntry !== 'object') {
396
+ return '';
397
+ }
398
+ return typeof toolResultEntry.content === 'string'
399
+ ? toolResultEntry.content
400
+ : JSON.stringify(toolResultEntry.content);
401
+ }
402
+
403
+ /**
404
+ * 获取 Claude Code 写入的结构化异步启动标记。
405
+ */
406
+ function getAsyncLaunchFlagFromRow(row) {
407
+ const result = row && row.toolUseResult;
408
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
409
+ return null;
410
+ }
411
+ return result.status === 'async_launched' || result.isAsync === true;
412
+ }
413
+
414
+ /**
415
+ * 判断工具返回是否代表异步 Agent 已启动。
416
+ */
417
+ function isAsyncAgentLaunchResult(toolResultEntry) {
418
+ if (!toolResultEntry || typeof toolResultEntry !== 'object') {
419
+ return false;
420
+ }
421
+ if (toolResultEntry.isAsyncLaunch != null) {
422
+ return Boolean(toolResultEntry.isAsyncLaunch);
423
+ }
424
+ const text = getToolResultText(toolResultEntry);
425
+ return text.includes('Async agent launched successfully')
426
+ || (text.includes('agentId:')
427
+ && text.includes('output_file:')
428
+ && text.includes('You will be notified automatically'));
429
+ }
430
+
431
+ /**
432
+ * 合并同一 message.id 的 Assistant 分片。
433
+ */
434
+ function mergeAssistantRows(rows) {
435
+ const base = { ...rows[rows.length - 1] };
436
+ const finalMessage = rows[rows.length - 1].message;
437
+ const message = finalMessage && typeof finalMessage === 'object'
438
+ ? { ...finalMessage }
439
+ : {};
440
+ const content = [];
441
+ for (const row of rows) {
442
+ const rowContent = row && row.message && row.message.content;
443
+ if (Array.isArray(rowContent)) {
444
+ content.push(...rowContent);
445
+ } else if (typeof rowContent === 'string' && rowContent) {
446
+ content.push({ type: 'text', text: rowContent });
447
+ }
448
+ }
449
+ message.content = content;
450
+ base.message = message;
451
+ return base;
452
+ }
453
+
454
+ /**
455
+ * 创建 Turn 组装过程的临时状态。
456
+ */
457
+ function createTurnAssemblyState() {
458
+ return {
459
+ currentUserRow: null,
460
+ assistantMessageIds: [],
461
+ assistantRowsByMessageId: {},
462
+ toolResultsById: {},
463
+ toolUseTimestampsById: {},
464
+ injectedByToolId: {},
465
+ currentRows: [],
466
+ };
467
+ }
468
+
469
+ /**
470
+ * 尝试把当前临时状态构造成完整 Turn。
471
+ */
472
+ function buildTurnFromState(state) {
473
+ if (!state.currentUserRow || state.assistantMessageIds.length === 0) {
474
+ return null;
475
+ }
476
+ const assistantMessages = state.assistantMessageIds
477
+ .map((messageId) => state.assistantRowsByMessageId[messageId])
478
+ .filter((rows) => Array.isArray(rows) && rows.length > 0)
479
+ .map(mergeAssistantRows);
480
+ if (assistantMessages.length === 0) {
481
+ return null;
482
+ }
483
+ return {
484
+ userMsg: state.currentUserRow,
485
+ assistantMsgs: assistantMessages,
486
+ toolResultsById: { ...state.toolResultsById },
487
+ toolUseTimestampsById: { ...state.toolUseTimestampsById },
488
+ injectedByToolId: { ...state.injectedByToolId },
489
+ rows: state.currentRows.slice(),
490
+ };
491
+ }
492
+
493
+ /**
494
+ * 使用真实用户消息开始新的 Turn。
495
+ */
496
+ function startNewTurn(row, state) {
497
+ state.currentUserRow = row;
498
+ state.assistantMessageIds = [];
499
+ state.assistantRowsByMessageId = {};
500
+ state.toolResultsById = {};
501
+ state.toolUseTimestampsById = {};
502
+ state.injectedByToolId = {};
503
+ state.currentRows = [row];
504
+ }
505
+
506
+ /**
507
+ * 向当前 Turn 添加 Assistant 分片及工具时间。
508
+ */
509
+ function addAssistantRow(row, state) {
510
+ if (!state.currentUserRow) {
511
+ return;
512
+ }
513
+ const messageId = getMessageId(row) || `noid:${state.assistantMessageIds.length}`;
514
+ if (!state.assistantRowsByMessageId[messageId]) {
515
+ state.assistantMessageIds.push(messageId);
516
+ state.assistantRowsByMessageId[messageId] = [];
517
+ }
518
+ state.assistantRowsByMessageId[messageId].push(row);
519
+ for (const toolUse of getToolUseBlocks(getContentFromRow(row))) {
520
+ if (toolUse.id && state.toolUseTimestampsById[String(toolUse.id)] == null) {
521
+ state.toolUseTimestampsById[String(toolUse.id)] = row.timestamp;
522
+ }
523
+ }
524
+ state.currentRows.push(row);
525
+ }
526
+
527
+ /**
528
+ * 处理 Skill 等 isMeta 注入行。
529
+ */
530
+ function addInjectedContextRow(row, state) {
531
+ if (!row || !row.isMeta) {
532
+ return false;
533
+ }
534
+ if (row.sourceToolUseID) {
535
+ const text = extractTextFromContent(getContentFromRow(row));
536
+ if (text) {
537
+ state.injectedByToolId[String(row.sourceToolUseID)] = text;
538
+ state.currentRows.push(row);
539
+ }
540
+ }
541
+ return true;
542
+ }
543
+
544
+ /**
545
+ * 处理普通 tool_result 行。
546
+ */
547
+ function addToolResultRow(row, state) {
548
+ if (!isToolResultRow(row)) {
549
+ return false;
550
+ }
551
+ state.currentRows.push(row);
552
+ const asyncFlag = getAsyncLaunchFlagFromRow(row);
553
+ for (const result of getToolResultBlocks(getContentFromRow(row))) {
554
+ if (!result.tool_use_id) {
555
+ continue;
556
+ }
557
+ const entry = {
558
+ content: result.content,
559
+ timestamp: row.timestamp,
560
+ };
561
+ if (asyncFlag != null) {
562
+ entry.isAsyncLaunch = asyncFlag;
563
+ }
564
+ state.toolResultsById[String(result.tool_use_id)] = entry;
565
+ }
566
+ return true;
567
+ }
568
+
569
+ /**
570
+ * 处理已路由到当前 Turn 的任务通知。
571
+ */
572
+ function addTaskNotificationRow(row, state, taskIdToToolUseId) {
573
+ if (!isTaskNotificationRow(row)) {
574
+ return false;
575
+ }
576
+ if (!state.currentUserRow) {
577
+ return true;
578
+ }
579
+ const toolUseId = getToolUseIdForTaskNotification(row, taskIdToToolUseId);
580
+ if (!toolUseId) {
581
+ state.currentRows.push(row);
582
+ return true;
583
+ }
584
+ const existing = state.toolResultsById[toolUseId];
585
+ if (existing && typeof existing === 'object') {
586
+ existing.finalContent = getResultFromTaskNotification(row);
587
+ existing.finalTimestamp = row.timestamp;
588
+ } else {
589
+ state.toolResultsById[toolUseId] = {
590
+ content: getResultFromTaskNotification(row),
591
+ timestamp: row.timestamp,
592
+ };
593
+ }
594
+ state.currentRows.push(row);
595
+ return true;
596
+ }
597
+
598
+ /**
599
+ * 将 transcript 行组装为对话 Turn。
600
+ */
601
+ function buildTurns(rows, taskIdToToolUseId = {}) {
602
+ const turns = [];
603
+ const state = createTurnAssemblyState();
604
+ for (const row of rows) {
605
+ if (addInjectedContextRow(row, state)
606
+ || addToolResultRow(row, state)
607
+ || addTaskNotificationRow(row, state, taskIdToToolUseId)) {
608
+ continue;
609
+ }
610
+ const role = getRoleFromRow(row);
611
+ if (role === 'user') {
612
+ const turn = buildTurnFromState(state);
613
+ if (turn) {
614
+ turns.push(turn);
615
+ }
616
+ startNewTurn(row, state);
617
+ } else if (role === 'assistant') {
618
+ addAssistantRow(row, state);
619
+ }
620
+ }
621
+ const finalTurn = buildTurnFromState(state);
622
+ if (finalTurn) {
623
+ turns.push(finalTurn);
624
+ }
625
+ return turns;
626
+ }
627
+
628
+ /**
629
+ * 找出 Turn 中尚未收到最终通知的异步 Agent 工具。
630
+ */
631
+ function getPendingAgentToolUseIds(turn) {
632
+ const pendingIds = [];
633
+ for (const assistantMessage of turn.assistantMsgs) {
634
+ for (const toolUse of getToolUseBlocks(getContentFromRow(assistantMessage))) {
635
+ if (toolUse.name !== 'Agent' && toolUse.name !== 'Task') {
636
+ continue;
637
+ }
638
+ const toolUseId = String(toolUse.id || '');
639
+ if (!toolUseId) {
640
+ continue;
641
+ }
642
+ const result = turn.toolResultsById[toolUseId];
643
+ if (result && result.finalContent != null) {
644
+ continue;
645
+ }
646
+ if (isAsyncAgentLaunchResult(result)) {
647
+ pendingIds.push(toolUseId);
648
+ }
649
+ }
650
+ }
651
+ return pendingIds;
652
+ }
653
+
654
+ /**
655
+ * 筛选当前可以导出的 Turn,并延迟异步 Agent Turn。
656
+ */
657
+ function getTurnsToEmit(turns, sessionState, flushDeferredAgentTurns = false) {
658
+ const ready = [];
659
+ for (const turn of turns) {
660
+ const pendingIds = getPendingAgentToolUseIds(turn);
661
+ if (pendingIds.length === 0 || flushDeferredAgentTurns) {
662
+ ready.push(turn);
663
+ continue;
664
+ }
665
+ sessionState.pendingAgentTurns.push({
666
+ pendingToolUseIds: pendingIds,
667
+ rows: turn.rows,
668
+ });
669
+ }
670
+ return ready;
671
+ }
672
+
673
+ /**
674
+ * 发现主 transcript 对应的子 Agent transcript。
675
+ */
676
+ function getSubagentTranscriptsByToolUseId(transcriptPath) {
677
+ const parsed = path.parse(transcriptPath);
678
+ const subagentDir = path.join(parsed.dir, parsed.name, 'subagents');
679
+ if (!fs.existsSync(subagentDir) || !fs.statSync(subagentDir).isDirectory()) {
680
+ return {};
681
+ }
682
+
683
+ const result = {};
684
+ for (const fileName of fs.readdirSync(subagentDir)) {
685
+ if (!fileName.endsWith('.meta.json')) {
686
+ continue;
687
+ }
688
+ const metaPath = path.join(subagentDir, fileName);
689
+ let metadata;
690
+ try {
691
+ metadata = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
692
+ } catch {
693
+ continue;
694
+ }
695
+ if (!metadata || typeof metadata.toolUseId !== 'string' || !metadata.toolUseId) {
696
+ continue;
697
+ }
698
+ const stem = fileName.slice(0, -'.meta.json'.length);
699
+ const jsonlPath = path.join(subagentDir, `${stem}.jsonl`);
700
+ if (!fs.existsSync(jsonlPath)) {
701
+ continue;
702
+ }
703
+ result[metadata.toolUseId] = {
704
+ path: jsonlPath,
705
+ agentId: stem.startsWith('agent-') ? stem.slice('agent-'.length) : stem,
706
+ agentType: metadata.agentType,
707
+ description: metadata.description,
708
+ };
709
+ }
710
+ return result;
711
+ }
712
+
713
+ /**
714
+ * 构造 task_id 到父级 tool_use_id 的映射。
715
+ */
716
+ function getTaskIdToToolUseId(subagents) {
717
+ const result = {};
718
+ for (const [toolUseId, subagent] of Object.entries(subagents || {})) {
719
+ if (subagent && typeof subagent.agentId === 'string' && subagent.agentId) {
720
+ result[subagent.agentId] = toolUseId;
721
+ }
722
+ }
723
+ return result;
724
+ }
725
+
726
+ /**
727
+ * 读取完整子 Agent JSONL,忽略损坏行。
728
+ */
729
+ function readSubagentJsonl(filePath) {
730
+ const rows = [];
731
+ const content = fs.readFileSync(filePath, 'utf8');
732
+ for (const rawLine of content.split(/\r?\n/)) {
733
+ const line = rawLine.trim();
734
+ if (!line) {
735
+ continue;
736
+ }
737
+ try {
738
+ const row = JSON.parse(line);
739
+ if (row && typeof row === 'object' && !Array.isArray(row)) {
740
+ rows.push(row);
741
+ }
742
+ } catch {
743
+ // 与原插件一致:损坏的子 Agent 行不阻断整条轨迹。
744
+ }
745
+ }
746
+ return rows;
747
+ }
748
+
749
+ /**
750
+ * 读取新增 transcript,并结合历史延迟状态生成 Turn。
751
+ */
752
+ function getNewTurnsFromTranscript(
753
+ transcriptPath,
754
+ sessionState,
755
+ subagents,
756
+ flushDeferredAgentTurns = false,
757
+ ) {
758
+ const readResult = readNewJsonl(transcriptPath, sessionState);
759
+ const taskMap = getTaskIdToToolUseId(subagents);
760
+ const routed = resolveDeferredAgentTurns(readResult.rows, sessionState, taskMap);
761
+ let deferredRows = routed.resolvedTurnRows;
762
+
763
+ if (flushDeferredAgentTurns && sessionState.pendingAgentTurns.length > 0) {
764
+ deferredRows = deferredRows.concat(popAllDeferredAgentTurnRows(sessionState));
765
+ }
766
+ if (flushDeferredAgentTurns) {
767
+ sessionState.pendingTaskNotifications = [];
768
+ }
769
+
770
+ const turns = [];
771
+ for (const rows of deferredRows) {
772
+ turns.push(...buildTurns(rows, taskMap));
773
+ }
774
+ if (routed.remainingRows.length > 0) {
775
+ turns.push(...buildTurns(routed.remainingRows, taskMap));
776
+ }
777
+ return { turns, sessionState };
778
+ }
779
+
780
+ module.exports = {
781
+ MAX_PENDING_TASK_NOTIFICATIONS,
782
+ buildTurns,
783
+ createSessionState,
784
+ extractTextFromContent,
785
+ getContentFromRow,
786
+ getModel,
787
+ getNewTurnsFromTranscript,
788
+ getPendingAgentToolUseIds,
789
+ getSubagentTranscriptsByToolUseId,
790
+ getTaskIdToToolUseId,
791
+ getToolResultText,
792
+ getToolResultBlocks,
793
+ getToolUseBlocks,
794
+ getTurnsToEmit,
795
+ getUsageDetailsFromRow,
796
+ isAsyncAgentLaunchResult,
797
+ isTaskNotificationRow,
798
+ parseTimestamp,
799
+ readNewJsonl,
800
+ readSubagentJsonl,
801
+ resolveDeferredAgentTurns,
802
+ truncateText,
803
+ };