@epoch-agent/view 3.0.0 → 3.1.2
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/index.d.ts +50 -2
- package/dist/index.js +21 -18
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -208,6 +208,16 @@ type HistoryItemDiff = HistoryItemBase & {
|
|
|
208
208
|
filePath: string;
|
|
209
209
|
oldContent?: string;
|
|
210
210
|
newContent?: string;
|
|
211
|
+
/**
|
|
212
|
+
* 内容太大、没存下来(2026-10-30)。
|
|
213
|
+
*
|
|
214
|
+
* 落盘那一侧对单侧内容设了上限(`runtime/src/turn-steps.ts` 的
|
|
215
|
+
* `MAX_DIFF_SIDE_BYTES`),超了的新旧内容一份都不存、只留路径和这一格。
|
|
216
|
+
*
|
|
217
|
+
* ⚠️ 渲染层遇到它要画一张**说清楚原因的降级卡**,不是空白、也不是一张
|
|
218
|
+
* 「整文件新增」的假补丁 —— 后者会在屏幕上冒充一个真实的改动。
|
|
219
|
+
*/
|
|
220
|
+
omitted?: 'too-large';
|
|
211
221
|
};
|
|
212
222
|
type HistoryItemToolGroup = HistoryItemBase & {
|
|
213
223
|
type: 'tool_group';
|
|
@@ -288,6 +298,38 @@ interface StreamReduceResult {
|
|
|
288
298
|
pending: HistoryItemWithoutId[];
|
|
289
299
|
/** 累加的 reasoning 文本(供状态栏显示) */
|
|
290
300
|
thought: string;
|
|
301
|
+
/**
|
|
302
|
+
* 末尾那个工具组**已经封口**了 —— 下一次 `tool-call` 要新起一组(2026-10-30)。
|
|
303
|
+
*
|
|
304
|
+
* ## 它治的是「收口那一刻屏幕上的顺序和刷新之后不一样」
|
|
305
|
+
*
|
|
306
|
+
* `turn-complete` 是一个 ReAct 步的结束(`core/src/agent/loop.ts`,判据在
|
|
307
|
+
* `runtime/src/turn-steps.ts` 的文件头)。以前这一帧在 `applyStreamEvent` 里
|
|
308
|
+
* 是**空操作**,于是「第 1 步调了 todo、第 2 步调了 schedule_draft」这两次调用
|
|
309
|
+
* 被并进**同一个** `tool_group`。
|
|
310
|
+
*
|
|
311
|
+
* 而落盘是**一步一行**、回放按行走(`web/src/state/replay.ts` 的 `pushAssistant`),
|
|
312
|
+
* 刷出来的是两个分开的工具组。两条路对同一轮给出两种形状,后果不是「长得不一样」:
|
|
313
|
+
*
|
|
314
|
+
* 1. 步与步之间那几段思考的「位置」是 `pending.length`(`web/src/state/thought-parts.ts`
|
|
315
|
+
* 的 `trackThought`)。两个工具组合并成一个之后,两步之间想的那几段撞在**同一个
|
|
316
|
+
* 下标**上,被并成一段,还一起落到这个合并后的工具组**后面** ——
|
|
317
|
+
* 屏幕上就是「思考跑到三张工具卡下面去了」;
|
|
318
|
+
* 2. 收口那一刻(`session-state: idle`)照的就是这份合并过的 pending,于是
|
|
319
|
+
* **同一轮在收口后、切走切回后、刷新后长成三种样子**。
|
|
320
|
+
*
|
|
321
|
+
* ## 为什么是一个标志而不是往 pending 里塞一样东西
|
|
322
|
+
*
|
|
323
|
+
* 往展示项里加「步边界」等于新增一种展示项,两个宿主都得跟着改(判据在
|
|
324
|
+
* `notice` 那一支的注释末尾);而这两条路真正需要的只有一句话:
|
|
325
|
+
* **这一步的工具已经调完了**。它就落在这个标志上,不落在任何会被渲染的项上。
|
|
326
|
+
*
|
|
327
|
+
* ⚠️ **同一步之内的连续调用照旧合并**(`tool-call` 那一支的判据是
|
|
328
|
+
* `!state.sealed`):一次模型请求里并行调三个工具,屏幕上仍然是一张卡 ——
|
|
329
|
+
* 那是 Gemini 的分组语义,回放的「一步一行」也是同一个口径(一条 assistant 行
|
|
330
|
+
* 带着它那一步的全部 `toolCalls`)。
|
|
331
|
+
*/
|
|
332
|
+
sealed?: boolean;
|
|
291
333
|
}
|
|
292
334
|
/**
|
|
293
335
|
* 这一帧信封上、`AgentEvent` 里没有的东西。
|
|
@@ -305,8 +347,14 @@ interface StreamEventMeta {
|
|
|
305
347
|
/**
|
|
306
348
|
* 把一个事件折叠进当前状态。纯函数:不改入参,返回新数组。
|
|
307
349
|
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
350
|
+
* **不改任何东西**的事件(`finish` / `usage` / `approval-request` / `question`,
|
|
351
|
+
* 以及末尾不是工具组时的 `turn-complete`)原样返回同一个引用,调用方可以用
|
|
352
|
+
* `next === prev` 判断。
|
|
353
|
+
*
|
|
354
|
+
* ⚠️ **`turn-complete` 2026-10-30 起可能返回新对象**:它要把末尾那个工具组封口
|
|
355
|
+
* (`StreamReduceResult.sealed`)。它仍然不改 `pending` 数组本身(引用不变),
|
|
356
|
+
* 只翻一个标志 —— 所以「`pending` 变了没有」和「`state` 变了没有」在这一帧上
|
|
357
|
+
* 是两个问题,别拿后者当前者。
|
|
310
358
|
*
|
|
311
359
|
* `meta` 只有浏览器传(信封上的 `seq`)。**它只在新建项时落一次** ——
|
|
312
360
|
* 追加进已有项(正文的第二个增量、并进同一个工具组的第二次调用)时不覆盖,
|
package/dist/index.js
CHANGED
|
@@ -29,21 +29,22 @@ function patchTool(pending, callId, patch) {
|
|
|
29
29
|
}
|
|
30
30
|
function applyStreamEvent(state, event, meta) {
|
|
31
31
|
const { pending, thought } = state;
|
|
32
|
+
const carry = (next) => state.sealed === void 0 || next.sealed !== void 0 ? next : { ...next, sealed: state.sealed };
|
|
32
33
|
switch (event.type) {
|
|
33
34
|
case "reasoning-delta":
|
|
34
|
-
return { pending, thought: thought + event.text };
|
|
35
|
+
return carry({ pending, thought: thought + event.text });
|
|
35
36
|
case "text-delta": {
|
|
36
37
|
if (!event.text) return state;
|
|
37
38
|
const last = pending[pending.length - 1];
|
|
38
39
|
if (last?.type === "assistant") {
|
|
39
40
|
const next = [...pending];
|
|
40
41
|
next[next.length - 1] = { ...last, text: last.text + event.text };
|
|
41
|
-
return { pending: next, thought };
|
|
42
|
+
return carry({ pending: next, thought });
|
|
42
43
|
}
|
|
43
|
-
return {
|
|
44
|
+
return carry({
|
|
44
45
|
pending: [...pending, { type: "assistant", text: event.text, ...anchor(meta) }],
|
|
45
46
|
thought
|
|
46
|
-
};
|
|
47
|
+
});
|
|
47
48
|
}
|
|
48
49
|
case "tool-call": {
|
|
49
50
|
const tool = {
|
|
@@ -53,14 +54,15 @@ function applyStreamEvent(state, event, meta) {
|
|
|
53
54
|
status: "pending"
|
|
54
55
|
};
|
|
55
56
|
const last = pending[pending.length - 1];
|
|
56
|
-
if (last?.type === "tool_group") {
|
|
57
|
+
if (last?.type === "tool_group" && state.sealed !== true) {
|
|
57
58
|
const next = [...pending];
|
|
58
59
|
next[next.length - 1] = { ...last, tools: [...last.tools, tool] };
|
|
59
|
-
return { pending: next, thought };
|
|
60
|
+
return carry({ pending: next, thought });
|
|
60
61
|
}
|
|
61
62
|
return {
|
|
62
63
|
pending: [...pending, { type: "tool_group", tools: [tool], ...anchor(meta) }],
|
|
63
|
-
thought
|
|
64
|
+
thought,
|
|
65
|
+
sealed: false
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
case "tool-output-delta": {
|
|
@@ -69,7 +71,7 @@ function applyStreamEvent(state, event, meta) {
|
|
|
69
71
|
...tool,
|
|
70
72
|
liveOutput: tailOf((tool.liveOutput ?? "") + event.text)
|
|
71
73
|
}));
|
|
72
|
-
return next ? { pending: next, thought } : state;
|
|
74
|
+
return next ? carry({ pending: next, thought }) : state;
|
|
73
75
|
}
|
|
74
76
|
case "tool-result": {
|
|
75
77
|
const next = patchTool(pending, event.callId, (tool) => {
|
|
@@ -81,10 +83,10 @@ function applyStreamEvent(state, event, meta) {
|
|
|
81
83
|
...event.artifacts && event.artifacts.length > 0 ? { artifacts: event.artifacts } : {}
|
|
82
84
|
};
|
|
83
85
|
});
|
|
84
|
-
return next ? { pending: next, thought } : state;
|
|
86
|
+
return next ? carry({ pending: next, thought }) : state;
|
|
85
87
|
}
|
|
86
88
|
case "diff":
|
|
87
|
-
return {
|
|
89
|
+
return carry({
|
|
88
90
|
pending: [
|
|
89
91
|
...pending,
|
|
90
92
|
{
|
|
@@ -96,26 +98,27 @@ function applyStreamEvent(state, event, meta) {
|
|
|
96
98
|
}
|
|
97
99
|
],
|
|
98
100
|
thought
|
|
99
|
-
};
|
|
101
|
+
});
|
|
100
102
|
case "notice":
|
|
101
|
-
return {
|
|
103
|
+
return carry({
|
|
102
104
|
pending: [...pending, { type: "info", text: event.text, ...anchor(meta) }],
|
|
103
105
|
thought
|
|
104
|
-
};
|
|
106
|
+
});
|
|
105
107
|
case "error":
|
|
106
|
-
return {
|
|
108
|
+
return carry({
|
|
107
109
|
pending: [...pending, { type: "error", text: event.message, ...anchor(meta) }],
|
|
108
110
|
thought
|
|
109
|
-
};
|
|
111
|
+
});
|
|
110
112
|
case "steer":
|
|
111
|
-
return {
|
|
113
|
+
return carry({
|
|
112
114
|
pending: [...pending, { type: "user", text: event.text, ...anchor(meta) }],
|
|
113
115
|
thought
|
|
114
|
-
};
|
|
116
|
+
});
|
|
117
|
+
case "turn-complete":
|
|
118
|
+
return pending[pending.length - 1]?.type === "tool_group" ? { ...state, sealed: true } : state;
|
|
115
119
|
case "run-start":
|
|
116
120
|
case "finish":
|
|
117
121
|
case "usage":
|
|
118
|
-
case "turn-complete":
|
|
119
122
|
case "approval-request":
|
|
120
123
|
case "question":
|
|
121
124
|
return state;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch-agent/view",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "epoch-agent 展示层纯逻辑:AgentEvent → 可渲染 item,tui 与 web 共用",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@epoch-agent/protocol": "3.
|
|
25
|
+
"@epoch-agent/protocol": "3.1.2"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"tsup": "^8.3.6",
|