@co0ontty/wand 1.29.6 → 1.30.3
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/dist/process-manager.js +5 -2
- package/dist/structured-session-manager.js +183 -25
- package/dist/types.d.ts +19 -0
- package/dist/web-ui/content/scripts.js +997 -220
- package/dist/web-ui/content/styles.css +364 -18
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/package.json +1 -1
package/dist/process-manager.js
CHANGED
|
@@ -634,7 +634,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
634
634
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
635
635
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
636
636
|
selectedModel: selectedModel ?? null,
|
|
637
|
-
|
|
637
|
+
// cols 上限 256:与 @wterm/dom WASM grid 的 maxCols 硬编码一致,
|
|
638
|
+
// 防止服务端按 >256 cols 让 Claude 用 CSI 绝对列定位写到 wterm 实际
|
|
639
|
+
// 渲染不到的列上(表现为"内容神奇复制下行")。
|
|
640
|
+
ptyCols: opts?.cols !== undefined ? clampDimension(opts.cols, 20, 256) : 120,
|
|
638
641
|
ptyRows: opts?.rows !== undefined ? clampDimension(opts.rows, 10, 160) : 36,
|
|
639
642
|
};
|
|
640
643
|
if (isClaudeProvider) {
|
|
@@ -986,7 +989,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
986
989
|
if (!record.ptyProcess || record.status !== "running") {
|
|
987
990
|
return this.snapshot(record);
|
|
988
991
|
}
|
|
989
|
-
const safeCols = clampDimension(cols, 20,
|
|
992
|
+
const safeCols = clampDimension(cols, 20, 256);
|
|
990
993
|
const safeRows = clampDimension(rows, 10, 160);
|
|
991
994
|
const changed = safeCols !== record.ptyCols || safeRows !== record.ptyRows;
|
|
992
995
|
record.ptyProcess.resize(safeCols, safeRows);
|
|
@@ -83,6 +83,84 @@ export function thinkingEffortToCodexFlag(effort) {
|
|
|
83
83
|
default: return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
function captureTaskMeta(blocks, registry) {
|
|
87
|
+
for (const b of blocks) {
|
|
88
|
+
if (b.type !== "tool_use")
|
|
89
|
+
continue;
|
|
90
|
+
if (registry.has(b.id))
|
|
91
|
+
continue;
|
|
92
|
+
const input = b.input ?? {};
|
|
93
|
+
// Claude SDK 把这类"派 subagent 干活"的内置工具叫做 "Agent",CLI/旧版本里
|
|
94
|
+
// 也叫过 "Task"。判定不靠工具名(容易随版本变),而是看 input 是否含有
|
|
95
|
+
// `subagent_type` 字段——这是 Agent/Task 系列的唯一标志。
|
|
96
|
+
const agentType = typeof input.subagent_type === "string" ? input.subagent_type : undefined;
|
|
97
|
+
if (!agentType && b.name !== "Task" && b.name !== "Agent")
|
|
98
|
+
continue;
|
|
99
|
+
const description = typeof input.description === "string" ? input.description : undefined;
|
|
100
|
+
registry.set(b.id, { agentType, description });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Stamp every block with `__subagent` meta keyed to `parentToolUseId`. When
|
|
105
|
+
* the id has no entry yet (rare race: subagent emits before we see the parent
|
|
106
|
+
* Task tool_use), we still stamp the bare taskId so the UI can group blocks;
|
|
107
|
+
* agentType / description backfill on later updates.
|
|
108
|
+
*/
|
|
109
|
+
function tagSubagentBlocks(blocks, parentToolUseId, registry) {
|
|
110
|
+
if (!parentToolUseId)
|
|
111
|
+
return blocks;
|
|
112
|
+
const meta = registry.get(parentToolUseId);
|
|
113
|
+
const stamp = {
|
|
114
|
+
taskId: parentToolUseId,
|
|
115
|
+
...(meta?.agentType ? { agentType: meta.agentType } : {}),
|
|
116
|
+
...(meta?.description ? { taskDescription: meta.description } : {}),
|
|
117
|
+
};
|
|
118
|
+
return blocks.map((block) => ({ ...block, __subagent: stamp }));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 给已被 captureTaskMeta 识别为 Task/Agent 的 tool_use block 本身也盖 __subagent 章。
|
|
122
|
+
* taskId 用自己的 block.id —— 与子消息的 parent_tool_use_id(也等于这个 id)保持一致,
|
|
123
|
+
* 前端 splitTurnBySubagent 按 taskId 分组时父 Task tool_use 和 SDK 转发的子消息能合并到同一段。
|
|
124
|
+
*/
|
|
125
|
+
function stampSelfTask(blocks, registry) {
|
|
126
|
+
return blocks.map((b) => {
|
|
127
|
+
if (b.type !== "tool_use")
|
|
128
|
+
return b;
|
|
129
|
+
if (b.__subagent)
|
|
130
|
+
return b; // 已盖章不重复(防止幂等问题)
|
|
131
|
+
const meta = registry.get(b.id);
|
|
132
|
+
if (!meta && b.name !== "Task" && b.name !== "Agent")
|
|
133
|
+
return b;
|
|
134
|
+
const stamp = {
|
|
135
|
+
taskId: b.id,
|
|
136
|
+
...(meta?.agentType ? { agentType: meta.agentType } : {}),
|
|
137
|
+
...(meta?.description ? { taskDescription: meta.description } : {}),
|
|
138
|
+
};
|
|
139
|
+
return { ...b, __subagent: stamp };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 当父 assistant 在 parentToolUseId === null 的 user turn 里收到 Task 工具的 tool_result 时,
|
|
144
|
+
* tagSubagentBlocks 不会被调用(它只在 parentToolUseId 非空时盖章)。这里按 tool_use_id
|
|
145
|
+
* 反查 registry,给这条 tool_result 单独盖章,让前端能把它归到同一个 subagent 段。
|
|
146
|
+
*/
|
|
147
|
+
function stampParentTaskResults(blocks, registry) {
|
|
148
|
+
return blocks.map((b) => {
|
|
149
|
+
if (b.type !== "tool_result")
|
|
150
|
+
return b;
|
|
151
|
+
if (b.__subagent)
|
|
152
|
+
return b;
|
|
153
|
+
const meta = registry.get(b.tool_use_id);
|
|
154
|
+
if (!meta)
|
|
155
|
+
return b;
|
|
156
|
+
const stamp = {
|
|
157
|
+
taskId: b.tool_use_id,
|
|
158
|
+
...(meta.agentType ? { agentType: meta.agentType } : {}),
|
|
159
|
+
...(meta.description ? { taskDescription: meta.description } : {}),
|
|
160
|
+
};
|
|
161
|
+
return { ...b, __subagent: stamp };
|
|
162
|
+
});
|
|
163
|
+
}
|
|
86
164
|
const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
87
165
|
/** Min interval between full saveSession() calls for an in-progress streaming turn.
|
|
88
166
|
* saveSession serializes the entire messages array, so doing it on every NDJSON
|
|
@@ -1342,6 +1420,9 @@ export class StructuredSessionManager {
|
|
|
1342
1420
|
const blocksByKey = new Map();
|
|
1343
1421
|
const keyOrder = [];
|
|
1344
1422
|
let toolResultSeq = 0;
|
|
1423
|
+
// 本轮 Task tool_use_id → meta map,由父 assistant 消息里的 Task tool_use
|
|
1424
|
+
// 填充;子 agent message(parent_tool_use_id 非空)来时用它给每个 block 盖章。
|
|
1425
|
+
const taskMetaRegistry = new Map();
|
|
1345
1426
|
// 估算单个 ContentBlock 的"信息体积"——文字 / thinking / tool input 长度之和。
|
|
1346
1427
|
// 用于 upsertBlocks 的防御性合并:同一 message.id 重发时,按位置取信息量更大的
|
|
1347
1428
|
// 那个版本,保证已经吐出的文字 / tool_use input 不会被一条更短的同 id 事件
|
|
@@ -1372,22 +1453,25 @@ export class StructuredSessionManager {
|
|
|
1372
1453
|
blocksByKey.set(key, blocks);
|
|
1373
1454
|
return;
|
|
1374
1455
|
}
|
|
1375
|
-
// claude -p 在同一 message.id 的多次 assistant
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1456
|
+
// claude -p 在同一 message.id 的多次 assistant 事件有两种观察到的协议:
|
|
1457
|
+
// a) **累积模式**:每次 event 的 content = 之前所有 blocks + 0~N 新 block,
|
|
1458
|
+
// 同位置类型一致。流式 text/thinking 的逐字增量属于这种。
|
|
1459
|
+
// b) **拼接模式**:SDK 把 thinking 和后续的 tool_use 拆成两条 event 给同
|
|
1460
|
+
// 一 msg.id 发出,第二条只带 tool_use,**不包含**之前的 thinking。
|
|
1461
|
+
// Opus 4.7 + claude-agent-sdk 实际跑下来就是这种。
|
|
1378
1462
|
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1463
|
+
// 老逻辑("同 index 类型不一致 → 保留 prev")只对 a) 友好,碰上 b) 会让第
|
|
1464
|
+
// 二条事件里的 tool_use 直接被丢掉——表现是 Agent / Read 等 tool_use 永远
|
|
1465
|
+
// 不出现在 messages 里,subagent 多角色无法关联 agentType 到父 Task。
|
|
1381
1466
|
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
// event 给出最终 turnState.result,compactContentBlocks 的 fallback
|
|
1387
|
-
// 才补回 text。用户反馈"文字消失,回复完成后又出现"就是这条路径。
|
|
1467
|
+
// 新规则:当类型不一致时,把新 block **追加**到 merged 末尾而非覆盖 prev。
|
|
1468
|
+
// 既兼容 a)(同位置同类型仍按累积取大),又兼容 b)(拼接的新类型 block
|
|
1469
|
+
// 进入末尾),还能挡住 b 早期版本里"短回退"的异常 frame(blocks.length
|
|
1470
|
+
// < prev.length 时直接拒绝)。
|
|
1388
1471
|
if (blocks.length < prev.length)
|
|
1389
1472
|
return;
|
|
1390
1473
|
const merged = [];
|
|
1474
|
+
const appendix = [];
|
|
1391
1475
|
for (let i = 0; i < blocks.length; i++) {
|
|
1392
1476
|
const a = prev[i];
|
|
1393
1477
|
const b = blocks[i];
|
|
@@ -1405,11 +1489,14 @@ export class StructuredSessionManager {
|
|
|
1405
1489
|
merged.push(blockVolume(b) >= blockVolume(a) ? b : a);
|
|
1406
1490
|
}
|
|
1407
1491
|
else {
|
|
1408
|
-
// 类型变了:保留 prev
|
|
1492
|
+
// 类型变了:保留 prev[i],把 incoming block 追加到末尾。
|
|
1409
1493
|
merged.push(a);
|
|
1494
|
+
appendix.push(b);
|
|
1410
1495
|
}
|
|
1411
1496
|
}
|
|
1412
1497
|
}
|
|
1498
|
+
for (const b of appendix)
|
|
1499
|
+
merged.push(b);
|
|
1413
1500
|
blocksByKey.set(key, merged);
|
|
1414
1501
|
};
|
|
1415
1502
|
const rebuildTurnBlocks = () => {
|
|
@@ -1503,8 +1590,19 @@ export class StructuredSessionManager {
|
|
|
1503
1590
|
const msgId = typeof parsed.message.id === "string" && parsed.message.id
|
|
1504
1591
|
? `assistant:${parsed.message.id}`
|
|
1505
1592
|
: `assistant:anon:${keyOrder.length}`;
|
|
1506
|
-
|
|
1507
|
-
|
|
1593
|
+
// parent_tool_use_id 决定父/子 agent。父 message 里的 Task tool_use 登记
|
|
1594
|
+
// 到 taskMetaRegistry;子 message 的每个 block 用 __subagent 盖章。
|
|
1595
|
+
const parentToolUseId = typeof parsed.parent_tool_use_id === "string" && parsed.parent_tool_use_id
|
|
1596
|
+
? parsed.parent_tool_use_id
|
|
1597
|
+
: null;
|
|
1598
|
+
if (parentToolUseId === null) {
|
|
1599
|
+
captureTaskMeta(extracted.content, taskMetaRegistry);
|
|
1600
|
+
}
|
|
1601
|
+
const stamped = parentToolUseId === null
|
|
1602
|
+
? stampSelfTask(extracted.content, taskMetaRegistry)
|
|
1603
|
+
: tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry);
|
|
1604
|
+
if (stamped.length > 0) {
|
|
1605
|
+
upsertBlocks(msgId, stamped);
|
|
1508
1606
|
rebuildTurnBlocks();
|
|
1509
1607
|
}
|
|
1510
1608
|
// NOTE: usage from streaming "assistant" events contains partial/incremental
|
|
@@ -1541,8 +1639,14 @@ export class StructuredSessionManager {
|
|
|
1541
1639
|
});
|
|
1542
1640
|
}
|
|
1543
1641
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1642
|
+
const parentToolUseId = typeof parsed.parent_tool_use_id === "string" && parsed.parent_tool_use_id
|
|
1643
|
+
? parsed.parent_tool_use_id
|
|
1644
|
+
: null;
|
|
1645
|
+
const stamped = parentToolUseId === null
|
|
1646
|
+
? stampParentTaskResults(collected, taskMetaRegistry)
|
|
1647
|
+
: tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry);
|
|
1648
|
+
if (stamped.length > 0) {
|
|
1649
|
+
upsertBlocks(`tool_result:${toolResultSeq++}`, stamped);
|
|
1546
1650
|
rebuildTurnBlocks();
|
|
1547
1651
|
}
|
|
1548
1652
|
syncSnapshot();
|
|
@@ -1794,6 +1898,10 @@ export class StructuredSessionManager {
|
|
|
1794
1898
|
...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
|
|
1795
1899
|
...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
|
|
1796
1900
|
includePartialMessages: true,
|
|
1901
|
+
// 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
|
|
1902
|
+
// 渲染成独立角色的群聊消息。关掉这个开关时只会收到子 agent 的 tool_use/tool_result,
|
|
1903
|
+
// text/thinking 被 SDK 吞掉。
|
|
1904
|
+
forwardSubagentText: true,
|
|
1797
1905
|
...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
|
|
1798
1906
|
...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
|
|
1799
1907
|
};
|
|
@@ -1855,6 +1963,11 @@ export class StructuredSessionManager {
|
|
|
1855
1963
|
// Tracks in-progress streaming blocks keyed by content_block index from stream_event.
|
|
1856
1964
|
// The map is cleared whenever a complete `assistant` message arrives — its blocks
|
|
1857
1965
|
// are then promoted into `finalizedBlocks` below.
|
|
1966
|
+
//
|
|
1967
|
+
// `parentToolUseId` carries through from SDKPartialAssistantMessage so we can
|
|
1968
|
+
// stamp streaming blocks with subagent persona *during* streaming, not only
|
|
1969
|
+
// after the completion event. Without it, subagent text shows up under the
|
|
1970
|
+
// parent's avatar for tens of ms then snaps to the subagent — visible flicker.
|
|
1858
1971
|
const streamingBlockByIndex = new Map();
|
|
1859
1972
|
// Blocks from messages that have already completed within this turn — including
|
|
1860
1973
|
// the parent assistant's prior messages, every subagent assistant message, and
|
|
@@ -1862,6 +1975,9 @@ export class StructuredSessionManager {
|
|
|
1862
1975
|
// back-to-back; without this list, each new streaming message would visually
|
|
1863
1976
|
// erase everything that came before it in the same turn.
|
|
1864
1977
|
const finalizedBlocks = [];
|
|
1978
|
+
// Per-turn Task tool_use_id → meta map; populated from the parent assistant's
|
|
1979
|
+
// Task tool_use blocks and consulted when subagent messages arrive.
|
|
1980
|
+
const taskMetaRegistry = new Map();
|
|
1865
1981
|
let emitTimer = null;
|
|
1866
1982
|
const flushEmit = () => {
|
|
1867
1983
|
if (emitTimer) {
|
|
@@ -1884,11 +2000,12 @@ export class StructuredSessionManager {
|
|
|
1884
2000
|
const sorted = [...streamingBlockByIndex.entries()].sort((a, b) => a[0] - b[0]);
|
|
1885
2001
|
const streaming = [];
|
|
1886
2002
|
for (const [, sb] of sorted) {
|
|
2003
|
+
let block = null;
|
|
1887
2004
|
if (sb.type === "text") {
|
|
1888
|
-
|
|
2005
|
+
block = { type: "text", text: sb.text };
|
|
1889
2006
|
}
|
|
1890
2007
|
else if (sb.type === "thinking") {
|
|
1891
|
-
|
|
2008
|
+
block = { type: "thinking", thinking: sb.thinking };
|
|
1892
2009
|
}
|
|
1893
2010
|
else if (sb.type === "tool_use" && sb.id && sb.name) {
|
|
1894
2011
|
let input = {};
|
|
@@ -1898,10 +2015,25 @@ export class StructuredSessionManager {
|
|
|
1898
2015
|
}
|
|
1899
2016
|
catch { /* partial json */ }
|
|
1900
2017
|
}
|
|
1901
|
-
|
|
2018
|
+
block = { type: "tool_use", id: sb.id, name: sb.name, input };
|
|
2019
|
+
}
|
|
2020
|
+
if (!block)
|
|
2021
|
+
continue;
|
|
2022
|
+
if (sb.parentToolUseId) {
|
|
2023
|
+
const [stamped] = tagSubagentBlocks([block], sb.parentToolUseId, taskMetaRegistry);
|
|
2024
|
+
streaming.push(stamped);
|
|
2025
|
+
}
|
|
2026
|
+
else {
|
|
2027
|
+
streaming.push(block);
|
|
1902
2028
|
}
|
|
1903
2029
|
}
|
|
1904
|
-
|
|
2030
|
+
// 流式阶段就给 Task/Agent tool_use 本身盖章,防止"先显示工具卡片几秒再跳为
|
|
2031
|
+
// handoff 行"的闪烁。content_block_start 阶段就有 name=Task/Agent,
|
|
2032
|
+
// stampSelfTask 据此即可命中;agentType 字段藏在 input 里,delta 累计后再由
|
|
2033
|
+
// 后续 captureTaskMeta 回填 registry,下次 rebuild 自动补上更完整的 stamp。
|
|
2034
|
+
captureTaskMeta(streaming, taskMetaRegistry);
|
|
2035
|
+
const stampedStreaming = stampSelfTask(streaming, taskMetaRegistry);
|
|
2036
|
+
return [...finalizedBlocks, ...stampedStreaming];
|
|
1905
2037
|
};
|
|
1906
2038
|
const syncSnapshot = () => {
|
|
1907
2039
|
const current = this.sessions.get(sessionId);
|
|
@@ -1950,7 +2082,9 @@ export class StructuredSessionManager {
|
|
|
1950
2082
|
break;
|
|
1951
2083
|
// Incremental streaming events (opt-in via includePartialMessages: true)
|
|
1952
2084
|
if (msg.type === "stream_event") {
|
|
1953
|
-
const
|
|
2085
|
+
const partial = msg;
|
|
2086
|
+
const ev = partial.event;
|
|
2087
|
+
const partialParentId = partial.parent_tool_use_id ?? null;
|
|
1954
2088
|
if (ev.type === "content_block_start") {
|
|
1955
2089
|
const cb = ev.content_block;
|
|
1956
2090
|
const blockType = cb.type;
|
|
@@ -1963,6 +2097,7 @@ export class StructuredSessionManager {
|
|
|
1963
2097
|
thinking: typeof cb.thinking === "string" ? cb.thinking : "",
|
|
1964
2098
|
partialInput: "",
|
|
1965
2099
|
finalized: false,
|
|
2100
|
+
parentToolUseId: partialParentId,
|
|
1966
2101
|
});
|
|
1967
2102
|
turnState.blocks = rebuildStreamingBlocks();
|
|
1968
2103
|
syncSnapshot();
|
|
@@ -2005,7 +2140,16 @@ export class StructuredSessionManager {
|
|
|
2005
2140
|
if (msg.type === "assistant") {
|
|
2006
2141
|
const assistantMsg = msg;
|
|
2007
2142
|
const extracted = this.extractAssistantMessage(assistantMsg.message);
|
|
2008
|
-
|
|
2143
|
+
// 父 assistant 的 Task tool_use → 注册到本轮 taskMeta map;
|
|
2144
|
+
// 子 agent 的 message(parent_tool_use_id 非空)→ 给每个 block 盖章。
|
|
2145
|
+
const parentToolUseId = assistantMsg.parent_tool_use_id ?? null;
|
|
2146
|
+
if (parentToolUseId === null) {
|
|
2147
|
+
captureTaskMeta(extracted.content, taskMetaRegistry);
|
|
2148
|
+
finalizedBlocks.push(...stampSelfTask(extracted.content, taskMetaRegistry));
|
|
2149
|
+
}
|
|
2150
|
+
else {
|
|
2151
|
+
finalizedBlocks.push(...tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry));
|
|
2152
|
+
}
|
|
2009
2153
|
streamingBlockByIndex.clear();
|
|
2010
2154
|
turnState.blocks = rebuildStreamingBlocks();
|
|
2011
2155
|
if (assistantMsg.session_id)
|
|
@@ -2041,11 +2185,13 @@ export class StructuredSessionManager {
|
|
|
2041
2185
|
// tool call, or a subagent's tool_result during Task execution).
|
|
2042
2186
|
if (msg.type === "user") {
|
|
2043
2187
|
const userMsg = msg;
|
|
2188
|
+
const parentToolUseId = userMsg.parent_tool_use_id ?? null;
|
|
2044
2189
|
const content = Array.isArray(userMsg.message?.content) ? userMsg.message.content : [];
|
|
2190
|
+
const collected = [];
|
|
2045
2191
|
for (const block of content) {
|
|
2046
2192
|
const b = block;
|
|
2047
2193
|
if (b?.type === "tool_result") {
|
|
2048
|
-
|
|
2194
|
+
collected.push({
|
|
2049
2195
|
type: "tool_result",
|
|
2050
2196
|
tool_use_id: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
|
|
2051
2197
|
content: this.normalizeToolResultContent(b.content),
|
|
@@ -2053,6 +2199,12 @@ export class StructuredSessionManager {
|
|
|
2053
2199
|
});
|
|
2054
2200
|
}
|
|
2055
2201
|
}
|
|
2202
|
+
if (parentToolUseId === null) {
|
|
2203
|
+
finalizedBlocks.push(...stampParentTaskResults(collected, taskMetaRegistry));
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
finalizedBlocks.push(...tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry));
|
|
2207
|
+
}
|
|
2056
2208
|
turnState.blocks = rebuildStreamingBlocks();
|
|
2057
2209
|
syncSnapshot();
|
|
2058
2210
|
scheduleEmit();
|
|
@@ -2227,11 +2379,17 @@ export class StructuredSessionManager {
|
|
|
2227
2379
|
const previous = compacted[compacted.length - 1];
|
|
2228
2380
|
if (previous
|
|
2229
2381
|
&& previous.type === "text"
|
|
2230
|
-
&& block.type === "text"
|
|
2382
|
+
&& block.type === "text"
|
|
2383
|
+
// 子 agent 边界不合并:父 assistant 的 text 与子 agent 的 text 必须保持独立,
|
|
2384
|
+
// 渲染层才能切段并给子 agent 单独发头像。同一 subagent 内部允许合并。
|
|
2385
|
+
&& (previous.__subagent?.taskId ?? null) === (block.__subagent?.taskId ?? null)) {
|
|
2231
2386
|
// 用新对象替换 compacted 末尾,**不要**就地改 previous.text —— previous
|
|
2232
2387
|
// 通常和调用方持有的 turnState.blocks 共享引用,原地 mutate 会让下次
|
|
2233
2388
|
// syncSnapshot 把已合并的内容再合并一次,呈指数级复制。
|
|
2234
|
-
|
|
2389
|
+
const merged = { type: "text", text: `${previous.text}${block.text}` };
|
|
2390
|
+
if (previous.__subagent)
|
|
2391
|
+
merged.__subagent = previous.__subagent;
|
|
2392
|
+
compacted[compacted.length - 1] = merged;
|
|
2235
2393
|
continue;
|
|
2236
2394
|
}
|
|
2237
2395
|
compacted.push(block);
|
package/dist/types.d.ts
CHANGED
|
@@ -299,13 +299,30 @@ export interface ChatMessage {
|
|
|
299
299
|
role: "user" | "assistant";
|
|
300
300
|
content: string;
|
|
301
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Meta marker attached to blocks emitted by a Task-spawned subagent. Present
|
|
304
|
+
* on every block (text / thinking / tool_use / tool_result) whose origin is a
|
|
305
|
+
* subagent's stream rather than the main assistant. Drives the multi-persona
|
|
306
|
+
* chat rendering ("third cat joining the conversation").
|
|
307
|
+
*
|
|
308
|
+
* `taskId` is the parent Task tool_use id (= SDK's `parent_tool_use_id`).
|
|
309
|
+
* The parent Task tool_use block itself is NOT marked — it lives in the
|
|
310
|
+
* main assistant's stream.
|
|
311
|
+
*/
|
|
312
|
+
export interface SubagentMeta {
|
|
313
|
+
taskId: string;
|
|
314
|
+
agentType?: string;
|
|
315
|
+
taskDescription?: string;
|
|
316
|
+
}
|
|
302
317
|
export interface TextBlock {
|
|
303
318
|
type: "text";
|
|
304
319
|
text: string;
|
|
320
|
+
__subagent?: SubagentMeta;
|
|
305
321
|
}
|
|
306
322
|
export interface ThinkingBlock {
|
|
307
323
|
type: "thinking";
|
|
308
324
|
thinking: string;
|
|
325
|
+
__subagent?: SubagentMeta;
|
|
309
326
|
}
|
|
310
327
|
export interface ToolUseBlock {
|
|
311
328
|
type: "tool_use";
|
|
@@ -313,6 +330,7 @@ export interface ToolUseBlock {
|
|
|
313
330
|
name: string;
|
|
314
331
|
description?: string;
|
|
315
332
|
input: Record<string, unknown>;
|
|
333
|
+
__subagent?: SubagentMeta;
|
|
316
334
|
}
|
|
317
335
|
export interface ToolResultBlock {
|
|
318
336
|
type: "tool_result";
|
|
@@ -324,6 +342,7 @@ export interface ToolResultBlock {
|
|
|
324
342
|
is_error?: boolean;
|
|
325
343
|
/** When true, content has been truncated for transport. Client should fetch full content via API. */
|
|
326
344
|
_truncated?: boolean;
|
|
345
|
+
__subagent?: SubagentMeta;
|
|
327
346
|
}
|
|
328
347
|
export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
|
|
329
348
|
export interface ConversationTurn {
|