minimal-agent 0.18.6 → 0.18.8
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/package.json +1 -1
- package/plugins/workflow-runner/src/runtime/context.js +5 -17
- package/plugins/workflow-runner/src/runtime/context.ts +4 -15
- package/plugins/workflow-runner/src/runtime/model.js +4 -2
- package/plugins/workflow-runner/src/runtime/model.ts +3 -2
- package/plugins/workflow-runner/test/api.test.ts +4 -2
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +5 -6
- package/plugins/workflow-runner/test/modelOutcome.test.ts +36 -0
- package/skills/dynamic-ui/SKILL.md +38 -278
- package/skills/dynamic-ui/mermaid.md +32 -0
- package/skills/dynamic-ui/scenes/architecture-and-flow.md +4 -1
- package/skills/dynamic-ui/scenes/data-visualization.md +5 -2
- package/skills/dynamic-ui/svg-design.md +251 -0
- package/skills/dynamic-ui/templates/README.md +2 -1
- package/skills/dynamic-ui/tokens/visual-tokens.md +4 -5
- package/src/advisor/README.md +24 -0
- package/src/advisor/advisorCoordinator.js +101 -45
- package/src/advisor/advisorCoordinator.ts +90 -51
- package/src/advisor/deliveryReview.js +35 -0
- package/src/advisor/deliveryReview.ts +30 -0
- package/src/advisor/presentation.js +10 -0
- package/src/advisor/presentation.ts +12 -0
- package/src/advisor/reviewContext.js +60 -0
- package/src/advisor/reviewContext.ts +72 -0
- package/src/context/persistContext.js +5 -1
- package/src/context/persistContext.ts +5 -2
- package/src/context/tokenCounter.js +3 -0
- package/src/context/tokenCounter.ts +2 -0
- package/src/llm/messageProvenance.js +1 -1
- package/src/llm/messageProvenance.ts +1 -1
- package/src/loop.js +226 -31
- package/src/loop.ts +205 -31
- package/src/memory/fileStore.js +2 -34
- package/src/memory/fileStore.ts +2 -26
- package/src/prompts/policyRegistry.js +13 -9
- package/src/prompts/policyRegistry.ts +15 -9
- package/src/prompts/promptAssembly.js +16 -23
- package/src/prompts/promptAssembly.ts +22 -27
- package/src/prompts/visualPresentation.js +28 -0
- package/src/prompts/visualPresentation.ts +26 -0
- package/src/runtime/agentRuntimeContext.js +3 -0
- package/src/runtime/agentRuntimeContext.ts +4 -0
- package/src/runtime/answerPreparation.js +9 -0
- package/src/runtime/answerPreparation.ts +11 -0
- package/src/runtime/eventSemantics.js +2 -0
- package/src/runtime/eventSemantics.ts +2 -0
- package/src/runtime/fileLock.js +75 -0
- package/src/runtime/fileLock.ts +73 -0
- package/src/runtime/finalAnswer.js +9 -0
- package/src/runtime/finalAnswer.ts +13 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- package/src/runtime/outputContract.js +17 -0
- package/src/runtime/outputContract.ts +34 -0
- package/src/session/runAgentTurn.js +7 -2
- package/src/session/runAgentTurn.ts +7 -2
- package/src/session/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- package/src/session/runtimeStore.js +11 -28
- package/src/session/runtimeStore.ts +9 -23
- package/src/tools/advisor/advisor.js +56 -40
- package/src/tools/advisor/advisor.ts +57 -39
- package/src/tools/advisor/advisorDecision.js +6 -3
- package/src/tools/advisor/advisorDecision.ts +6 -3
- package/src/tools/agent/agent.js +14 -6
- package/src/tools/agent/agent.ts +14 -5
- package/src/tools/bash/semantics.js +91 -26
- package/src/tools/bash/semantics.ts +63 -33
- package/src/tools/index.js +4 -1
- package/src/tools/index.ts +4 -1
- package/src/tools/skill/searchSkills.js +2 -2
- package/src/tools/skill/searchSkills.ts +2 -2
- package/src/tools/skill/skill.js +4 -0
- package/src/tools/skill/skill.ts +3 -0
- package/src/types.ts +19 -6
- package/src/ui/LiveArea.js +2 -1
- package/src/ui/LiveArea.tsx +2 -1
- package/src/ui/MessageList.js +18 -5
- package/src/ui/MessageList.tsx +25 -3
- package/src/ui/draftNotice.js +1 -8
- package/src/ui/draftNotice.ts +1 -10
- package/src/ui/hooks/useChat.js +25 -1
- package/src/ui/hooks/useChat.ts +22 -1
- package/src/web/protocol.ts +3 -1
|
@@ -21,35 +21,72 @@ export type CommandSemanticFn = (
|
|
|
21
21
|
stderr: string,
|
|
22
22
|
) => CommandSemanticResult;
|
|
23
23
|
|
|
24
|
+
/** Small conservative lexer, not a shell evaluator. Quoted regex operators are
|
|
25
|
+
* data. Expansions, redirects and unsupported syntax remain non-read-only. */
|
|
26
|
+
function inspectionCommands(command: string): string[][] | null {
|
|
27
|
+
const commands: string[][] = [];
|
|
28
|
+
let words: string[] = [];
|
|
29
|
+
let word = '';
|
|
30
|
+
let started = false;
|
|
31
|
+
let quote = '';
|
|
32
|
+
const flushWord = () => { if (started) words.push(word); word = ''; started = false; };
|
|
33
|
+
const flushCommand = () => { flushWord(); if (words.length) commands.push(words); words = []; };
|
|
34
|
+
for (let i = 0; i < command.length; i++) {
|
|
35
|
+
const c = command[i]!;
|
|
36
|
+
if (quote === "'") {
|
|
37
|
+
if (c === "'") quote = ''; else word += c;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (c === '\\') {
|
|
41
|
+
const next = command[++i];
|
|
42
|
+
if (next === undefined) return null;
|
|
43
|
+
if (quote === '"' && !['$', '`', '"', '\\', '\n'].includes(next)) word += '\\';
|
|
44
|
+
if (next !== '\n') { word += next; started = true; }
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (c === '`' || (c === '$' && /[({\w?'"*#@!$-]/.test(command[i + 1] ?? ''))) return null;
|
|
48
|
+
if (quote === '"') {
|
|
49
|
+
if (c === '"') quote = ''; else word += c;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (c === "'" || c === '"') { quote = c; started = true; continue; }
|
|
53
|
+
if (/[<>(){}]/.test(c)) return null;
|
|
54
|
+
if (c === '#' && !started) {
|
|
55
|
+
while (i + 1 < command.length && command[i + 1] !== '\n') i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === '&' && command[i + 1] !== '&') return null;
|
|
59
|
+
if (c === '&' || c === '|' || c === ';' || c === '\n') {
|
|
60
|
+
flushCommand();
|
|
61
|
+
if ((c === '&' || c === '|') && command[i + 1] === c) i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (/\s/.test(c)) { flushWord(); continue; }
|
|
65
|
+
word += c; started = true;
|
|
66
|
+
}
|
|
67
|
+
if (quote) return null;
|
|
68
|
+
flushCommand();
|
|
69
|
+
return commands;
|
|
70
|
+
}
|
|
71
|
+
|
|
24
72
|
/** Conservative shell side-effect classification used by Advisor gates. */
|
|
25
73
|
export function isReadOnlyShellCommand(command: string): boolean {
|
|
26
74
|
if (!command.trim()) return true;
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
34
|
-
const segments = body
|
|
35
|
-
.split(/&&|\|\||[;|\n]/)
|
|
36
|
-
.map((part) => part.trim())
|
|
37
|
-
.filter(Boolean);
|
|
38
|
-
for (const segment of segments) {
|
|
39
|
-
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
75
|
+
// A conservative scheduling hint, not an OS sandbox. Strict read-only
|
|
76
|
+
// consumers must reject shell execution rather than trust this heuristic.
|
|
77
|
+
// Do not strip nested shell quoting: expansion may happen in the outer shell.
|
|
78
|
+
const segments = inspectionCommands(command);
|
|
79
|
+
if (!segments) return false;
|
|
80
|
+
for (const tokens of segments) {
|
|
40
81
|
let index = 0;
|
|
41
82
|
if (
|
|
42
83
|
tokens[index] === 'command' &&
|
|
43
84
|
/^(?:-v|--verbose|-V)$/.test(tokens[index + 1] ?? '')
|
|
44
85
|
)
|
|
45
86
|
continue;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index]!)
|
|
50
|
-
)
|
|
51
|
-
index++;
|
|
52
|
-
const name = tokens[index]?.replace(/^.*\//, '').toLowerCase();
|
|
87
|
+
if (tokens[index] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? '')) return false;
|
|
88
|
+
if (/[\\/]/.test(tokens[index] ?? '')) return false;
|
|
89
|
+
const name = tokens[index]?.toLowerCase();
|
|
53
90
|
if (
|
|
54
91
|
!name ||
|
|
55
92
|
name === 'cd' ||
|
|
@@ -158,7 +195,7 @@ export function isReadOnlyShellCommand(command: string): boolean {
|
|
|
158
195
|
if ((name === 'env' || name === 'date') && tokens.length === index + 1)
|
|
159
196
|
continue;
|
|
160
197
|
if (
|
|
161
|
-
/^(?:node|bun|
|
|
198
|
+
/^(?:node|bun|python|python3)$/.test(name) &&
|
|
162
199
|
tokens.length === index + 2 &&
|
|
163
200
|
/^(?:--version|-v|--help|-h)$/.test(tokens[index + 1]!)
|
|
164
201
|
)
|
|
@@ -184,19 +221,12 @@ export function isReadOnlyShellCommand(command: string): boolean {
|
|
|
184
221
|
*/
|
|
185
222
|
export function isVerificationShellCommand(command: string): boolean {
|
|
186
223
|
if (!command.trim()) return true;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])([\s\S]*)\1\s*$/,
|
|
190
|
-
'$2',
|
|
191
|
-
);
|
|
192
|
-
const segments = body
|
|
193
|
-
.split(/&&|\|\||[;|\n]/)
|
|
194
|
-
.map((part) => part.trim())
|
|
195
|
-
.filter(Boolean);
|
|
224
|
+
const segments = inspectionCommands(command);
|
|
225
|
+
if (!segments) return false;
|
|
196
226
|
if (!segments.length) return false;
|
|
197
|
-
for (const
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
for (const tokens of segments) {
|
|
228
|
+
// Re-quote tokens for the shared classifier without reinterpreting data.
|
|
229
|
+
if (isReadOnlyShellCommand(tokens.map(token => `'${token.replace(/'/g, `'\\''`)}'`).join(' '))) continue;
|
|
200
230
|
let index = 0;
|
|
201
231
|
while (index < tokens.length && tokens[index] === 'sudo') index++;
|
|
202
232
|
while (
|
package/src/tools/index.js
CHANGED
|
@@ -204,7 +204,10 @@ export async function executeTool(name, rawArguments, signal, ctx) {
|
|
|
204
204
|
const issues = checked.error.issues
|
|
205
205
|
.map((i) => {
|
|
206
206
|
const where = i.path.join('.') || '<root>';
|
|
207
|
-
|
|
207
|
+
const arrayHint = i.code === 'invalid_type' && i.expected === 'string' && i.received === 'array'
|
|
208
|
+
? '\n 此处必须是字符串;列表用一层字符串数组,例如 ["第一条", "第二条"],不要 [["第一条"]] 或 XML item 标签。'
|
|
209
|
+
: '';
|
|
210
|
+
return ` - ${where}:${i.message}\n 收到的值:${seen(i.path)}${arrayHint}`;
|
|
208
211
|
})
|
|
209
212
|
.join('\n');
|
|
210
213
|
return {
|
package/src/tools/index.ts
CHANGED
|
@@ -249,7 +249,10 @@ export async function executeTool(
|
|
|
249
249
|
const issues = checked.error.issues
|
|
250
250
|
.map((i) => {
|
|
251
251
|
const where = i.path.join('.') || '<root>';
|
|
252
|
-
|
|
252
|
+
const arrayHint = i.code === 'invalid_type' && i.expected === 'string' && i.received === 'array'
|
|
253
|
+
? '\n 此处必须是字符串;列表用一层字符串数组,例如 ["第一条", "第二条"],不要 [["第一条"]] 或 XML item 标签。'
|
|
254
|
+
: '';
|
|
255
|
+
return ` - ${where}:${i.message}\n 收到的值:${seen(i.path)}${arrayHint}`;
|
|
253
256
|
})
|
|
254
257
|
.join('\n');
|
|
255
258
|
return {
|
|
@@ -24,7 +24,7 @@ export const searchSkillsTool = {
|
|
|
24
24
|
}),
|
|
25
25
|
callWithContext: async (input, signal, ctx) => {
|
|
26
26
|
signal?.throwIfAborted();
|
|
27
|
-
const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx));
|
|
27
|
+
const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx), ctx.availableToolNames?.());
|
|
28
28
|
signal?.throwIfAborted();
|
|
29
29
|
const terms = (input.query ?? '')
|
|
30
30
|
.toLocaleLowerCase()
|
|
@@ -48,7 +48,7 @@ export const searchSkillsTool = {
|
|
|
48
48
|
total: ranked.length,
|
|
49
49
|
nextOffset: offset + page.length < ranked.length ? offset + page.length : null,
|
|
50
50
|
skills: page.map(({ skill }) => skill),
|
|
51
|
-
note: '
|
|
51
|
+
note: '宿主提供有效工具集合时已按依赖筛选;目录不授予执行权限。同名项用 sourceId 精确加载,仅加载当前阶段相关技能。',
|
|
52
52
|
}),
|
|
53
53
|
};
|
|
54
54
|
},
|
|
@@ -28,7 +28,7 @@ export const searchSkillsTool: Tool<z.infer<typeof schema>> = {
|
|
|
28
28
|
}),
|
|
29
29
|
callWithContext: async (input, signal, ctx) => {
|
|
30
30
|
signal?.throwIfAborted();
|
|
31
|
-
const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx));
|
|
31
|
+
const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx), ctx.availableToolNames?.());
|
|
32
32
|
signal?.throwIfAborted();
|
|
33
33
|
const terms = (input.query ?? '')
|
|
34
34
|
.toLocaleLowerCase()
|
|
@@ -58,7 +58,7 @@ export const searchSkillsTool: Tool<z.infer<typeof schema>> = {
|
|
|
58
58
|
nextOffset:
|
|
59
59
|
offset + page.length < ranked.length ? offset + page.length : null,
|
|
60
60
|
skills: page.map(({ skill }) => skill),
|
|
61
|
-
note: '
|
|
61
|
+
note: '宿主提供有效工具集合时已按依赖筛选;目录不授予执行权限。同名项用 sourceId 精确加载,仅加载当前阶段相关技能。',
|
|
62
62
|
}),
|
|
63
63
|
};
|
|
64
64
|
},
|
package/src/tools/skill/skill.js
CHANGED
|
@@ -27,6 +27,10 @@ async function call(input, context) {
|
|
|
27
27
|
ok: false,
|
|
28
28
|
error: `Skill 名称有歧义:${input.name}。请选择 sourceId 后重试。\n${JSON.stringify(found.map(({ sourceId, sourceDir }) => ({ sourceId, sourceDir })))}`,
|
|
29
29
|
};
|
|
30
|
+
const available = context.availableToolNames?.();
|
|
31
|
+
const missing = available && found[0].requiresTools?.filter(name => !available.has(name));
|
|
32
|
+
if (missing?.length)
|
|
33
|
+
return { ok: false, error: `Skill ${input.name} 当前缺少工具:${missing.join(', ')}。不能通过技能扩大执行权限。` };
|
|
30
34
|
// Search roots are ordered: project skill intentionally overrides the bundled skill.
|
|
31
35
|
const path = join(found[0].sourceDir, 'SKILL.md');
|
|
32
36
|
const content = await readFile(path);
|
package/src/tools/skill/skill.ts
CHANGED
|
@@ -42,6 +42,9 @@ async function call(
|
|
|
42
42
|
ok: false,
|
|
43
43
|
error: `Skill 名称有歧义:${input.name}。请选择 sourceId 后重试。\n${JSON.stringify(found.map(({ sourceId, sourceDir }) => ({ sourceId, sourceDir })))}`,
|
|
44
44
|
};
|
|
45
|
+
const available = context.availableToolNames?.();
|
|
46
|
+
const missing = available && found[0]!.requiresTools?.filter(name => !available.has(name));
|
|
47
|
+
if (missing?.length) return { ok: false, error: `Skill ${input.name} 当前缺少工具:${missing.join(', ')}。不能通过技能扩大执行权限。` };
|
|
45
48
|
// Search roots are ordered: project skill intentionally overrides the bundled skill.
|
|
46
49
|
const path = join(found[0]!.sourceDir, 'SKILL.md');
|
|
47
50
|
const content = await readFile(path);
|
package/src/types.ts
CHANGED
|
@@ -163,6 +163,8 @@ export type Message = (
|
|
|
163
163
|
_ephemeral?: true;
|
|
164
164
|
/** Display-only milestone, persisted locally but excluded from model requests. */
|
|
165
165
|
_uiProgress?: true;
|
|
166
|
+
/** Local reading history only; never a model instruction or final answer. */
|
|
167
|
+
draftPresentation?: { reason: string; label: string };
|
|
166
168
|
workflowPresentation?: import('./runtime/workflowPresentation.js').WorkflowPresentation;
|
|
167
169
|
}
|
|
168
170
|
| {
|
|
@@ -174,6 +176,8 @@ export type Message = (
|
|
|
174
176
|
}
|
|
175
177
|
| {
|
|
176
178
|
role: 'assistant';
|
|
179
|
+
/** Persisted UI semantics; not a provider instruction or a success claim. */
|
|
180
|
+
presentation?: 'progress' | 'final';
|
|
177
181
|
/** Host-only review boundary for a delivered text answer; never model authority. */
|
|
178
182
|
advisorTaskVersion?: number;
|
|
179
183
|
/** 当 tool_calls 非空时 content 通常为 null(OpenAI 协议) */
|
|
@@ -566,6 +570,10 @@ export type ToolResult = { problem?: ToolProblemOutcome; observation?: ReadObser
|
|
|
566
570
|
* 避免按工具名写特判分支。
|
|
567
571
|
*/
|
|
568
572
|
export interface ToolCallContext {
|
|
573
|
+
/** Host presentation only; cannot grant tools to a delegated agent. */
|
|
574
|
+
output?: import('./runtime/outputContract.js').OutputContract;
|
|
575
|
+
/** Live effective catalog, including per-turn restrictions. */
|
|
576
|
+
availableToolNames?: () => ReadonlySet<string>;
|
|
569
577
|
messageProvenance?: ConversationProvenance;
|
|
570
578
|
agentHandoff?: {
|
|
571
579
|
questionIds?: readonly string[];
|
|
@@ -593,6 +601,8 @@ export interface ToolCallContext {
|
|
|
593
601
|
advisorInvocation?: 'agent' | 'manual';
|
|
594
602
|
/** Advisor 协调器运行时桥;只存在于主循环,不暴露给模型或持久化。 */
|
|
595
603
|
advisorRuntime?: AdvisorRuntimeBridge;
|
|
604
|
+
/** Live host context used by the main request, not tool-provided instructions. */
|
|
605
|
+
advisorReviewContext?: () => import('./advisor/reviewContext.js').AdvisorReviewContext;
|
|
596
606
|
/**
|
|
597
607
|
* 主循环使用的 provider —— Agent 等「工具内部再起子查询」的派生工具用它
|
|
598
608
|
* 跑嵌套 runQuery(子 agent 与主 agent 同一模型,不另配凭据)。
|
|
@@ -1122,8 +1132,8 @@ export type LlmStreamEvent =
|
|
|
1122
1132
|
* 工作过程显示阶段标签(UI 的 LiveArea 据此选择文字 / 决定是否隐藏整面板)。
|
|
1123
1133
|
*
|
|
1124
1134
|
* idle —— 当前没有任何工作(隐藏 LiveArea)
|
|
1125
|
-
* thinking ——
|
|
1126
|
-
* streaming ——
|
|
1135
|
+
* thinking —— 正在准备候选答案或等待工具调用,候选正文不公开
|
|
1136
|
+
* streaming —— 内部检查结束后,最终表达请求的真实正文流
|
|
1127
1137
|
* tool_executing —— 至少一个工具在跑(并行时栈式显示多个)
|
|
1128
1138
|
* compacting —— 正在跑自动 / reactive / 手动压缩
|
|
1129
1139
|
*/
|
|
@@ -1221,8 +1231,9 @@ export type LoopEvent =
|
|
|
1221
1231
|
message: Extract<Message, { role: 'assistant' }>;
|
|
1222
1232
|
}
|
|
1223
1233
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1234
|
+
* 内部候选正文尚不能交付,需要进入检查或补全阶段。
|
|
1235
|
+
* 新内核候选正文不广播;textLength=0,仅表示准备/复核阶段变化。
|
|
1236
|
+
* textLength>0 仅供旧事件兼容,不应作为正常展示路径。
|
|
1226
1237
|
*/
|
|
1227
1238
|
| {
|
|
1228
1239
|
type: 'assistant_draft_discarded';
|
|
@@ -1315,7 +1326,8 @@ export type LoopEvent =
|
|
|
1315
1326
|
| 'completed'
|
|
1316
1327
|
| 'delivery_incomplete'
|
|
1317
1328
|
| 'delivery_unverified'
|
|
1318
|
-
| 'claims_unverified'
|
|
1329
|
+
| 'claims_unverified'
|
|
1330
|
+
| 'no_answer';
|
|
1319
1331
|
warning?: string;
|
|
1320
1332
|
/** 本回合的资源账本。见 TurnResourceUsage —— 消费方务必先看 `trustworthy`。 */
|
|
1321
1333
|
usage?: TurnResourceUsage;
|
|
@@ -1336,7 +1348,7 @@ export type LoopEvent =
|
|
|
1336
1348
|
* - reason: 'manual' 用户按ESC/Ctrl+C手动中断
|
|
1337
1349
|
* - reason: 'queued_input' 有新的排队输入需要在安全点处理
|
|
1338
1350
|
*/
|
|
1339
|
-
| { type: 'interrupted'; reason?: 'manual' | 'queued_input' }
|
|
1351
|
+
| { type: 'interrupted'; reason?: 'manual' | 'queued_input'; usage?: TurnResourceUsage }
|
|
1340
1352
|
/**
|
|
1341
1353
|
* 出错(API 报错 / 网络异常等)。
|
|
1342
1354
|
*
|
|
@@ -1357,6 +1369,7 @@ export type LoopEvent =
|
|
|
1357
1369
|
| {
|
|
1358
1370
|
type: 'error';
|
|
1359
1371
|
error: string;
|
|
1372
|
+
usage?: TurnResourceUsage;
|
|
1360
1373
|
code?:
|
|
1361
1374
|
| 'llm_error'
|
|
1362
1375
|
| 'max_turns'
|
package/src/ui/LiveArea.js
CHANGED
|
@@ -37,7 +37,7 @@ import { figures } from './figures.js';
|
|
|
37
37
|
import { humanizeToolProgress } from './toolProgress.js';
|
|
38
38
|
const STAGE_LABELS = {
|
|
39
39
|
thinking: '思考中',
|
|
40
|
-
streaming: '
|
|
40
|
+
streaming: '正在输出最终答案',
|
|
41
41
|
tool_executing: '工作中',
|
|
42
42
|
compacting: '压缩历史',
|
|
43
43
|
};
|
|
@@ -179,6 +179,7 @@ export function buildLiveView(props) {
|
|
|
179
179
|
contentDim = true;
|
|
180
180
|
}
|
|
181
181
|
else if (stage === 'streaming' && streamingText.length > 0) {
|
|
182
|
+
// Compatibility for older event producers; new loops publish only approved answers.
|
|
182
183
|
contentLines = lastN(wrapToDisplayLines(streamingText, maxWidth), maxLines);
|
|
183
184
|
contentDim = false; // 答案 = 主角,默认亮色
|
|
184
185
|
}
|
package/src/ui/LiveArea.tsx
CHANGED
|
@@ -66,7 +66,7 @@ export interface LiveAreaProps {
|
|
|
66
66
|
|
|
67
67
|
const STAGE_LABELS: Record<Exclude<ProgressStage, 'idle'>, string> = {
|
|
68
68
|
thinking: '思考中',
|
|
69
|
-
streaming: '
|
|
69
|
+
streaming: '正在输出最终答案',
|
|
70
70
|
tool_executing: '工作中',
|
|
71
71
|
compacting: '压缩历史',
|
|
72
72
|
};
|
|
@@ -262,6 +262,7 @@ export function buildLiveView(
|
|
|
262
262
|
);
|
|
263
263
|
contentDim = true;
|
|
264
264
|
} else if (stage === 'streaming' && streamingText.length > 0) {
|
|
265
|
+
// Compatibility for older event producers; new loops publish only approved answers.
|
|
265
266
|
contentLines = lastN(wrapToDisplayLines(streamingText, maxWidth), maxLines);
|
|
266
267
|
contentDim = false; // 答案 = 主角,默认亮色
|
|
267
268
|
} else if (stage === 'tool_executing' && runningTools.length > 0) {
|
package/src/ui/MessageList.js
CHANGED
|
@@ -35,6 +35,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
35
35
|
import React from 'react';
|
|
36
36
|
import { Box, Static, Text } from 'ink';
|
|
37
37
|
import { conciseToolDescription } from './toolProgress.js';
|
|
38
|
+
import { advisorOpinionText, advisorDecisionLabel, advisorEvidenceLabel } from '../advisor/presentation.js';
|
|
38
39
|
import { contentToDisplayText, contentToPlainText } from '../multimodal.js';
|
|
39
40
|
import { splitThinkBlocks } from '../utils/stripThink.js';
|
|
40
41
|
import { figures } from './figures.js';
|
|
@@ -285,9 +286,11 @@ function MessageRow({ message, turnMeta, toolMeta, advisorDecision, foldReasonin
|
|
|
285
286
|
const ephemeral = message._ephemeral === true;
|
|
286
287
|
if (!ephemeral)
|
|
287
288
|
return null;
|
|
288
|
-
const lines =
|
|
289
|
+
const lines = message.draftPresentation
|
|
290
|
+
? renderMarkdownLines(terminalText(message.content)).map(line => line.text)
|
|
291
|
+
: terminalText(message.content).split('\n');
|
|
289
292
|
const milestone = message._uiProgress === true;
|
|
290
|
-
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children:
|
|
293
|
+
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsxs(Box, { flexDirection: "column", children: [message.draftPresentation && _jsx(Text, { color: theme.subtle, children: message.draftPresentation.label }), lines.map((line, i) => (_jsx(Text, { color: milestone ? theme.answer : theme.subtle, dimColor: !milestone, wrap: "end", children: line }, `eph-${i}`)))] }) }));
|
|
291
294
|
}
|
|
292
295
|
case 'user': {
|
|
293
296
|
const rule = figures.hr.repeat(6);
|
|
@@ -326,7 +329,7 @@ function MessageRow({ message, turnMeta, toolMeta, advisorDecision, foldReasonin
|
|
|
326
329
|
const hiddenReasoningCount = toolOnly
|
|
327
330
|
? 0
|
|
328
331
|
: reasoningLines.length - visibleReasoningLines.length;
|
|
329
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [visibleReasoningLines.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [visibleReasoningLines.map((line, i) => (_jsx(Text, { color: theme.subtle, dimColor: true, wrap: "end", children: i === 0 ? `${figures.thought} ${line}` : ` ${line}` }, `r-${i}`))), hiddenReasoningCount > 0 && (_jsx(Text, { color: theme.subtle, dimColor: true, children: ` ${figures.fold} ${hiddenReasoningCount} 行 reasoning 折叠(终端高度 < ${NARROW_TERMINAL_ROWS})` })), truncatedCharsHint.length > 0 && (_jsx(Text, { color: theme.subtle, dimColor: true, children: ` ${truncatedCharsHint}` }))] })), buildAssistantDisplayLines(answer).map((line, i) => {
|
|
332
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [visibleReasoningLines.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [visibleReasoningLines.map((line, i) => (_jsx(Text, { color: theme.subtle, dimColor: true, wrap: "end", children: i === 0 ? `${figures.thought} ${line}` : ` ${line}` }, `r-${i}`))), hiddenReasoningCount > 0 && (_jsx(Text, { color: theme.subtle, dimColor: true, children: ` ${figures.fold} ${hiddenReasoningCount} 行 reasoning 折叠(终端高度 < ${NARROW_TERMINAL_ROWS})` })), truncatedCharsHint.length > 0 && (_jsx(Text, { color: theme.subtle, dimColor: true, children: ` ${truncatedCharsHint}` }))] })), answer.trim() && (message.presentation || tcCount > 0) && (_jsx(Text, { color: theme.subtle, dimColor: true, children: message.presentation === 'final' ? '最终回答' : '工作进展 · 执行中说明' })), buildAssistantDisplayLines(answer).map((line, i) => {
|
|
330
333
|
if (line.kind === 'fence') {
|
|
331
334
|
return (_jsx(Text, { color: theme.subtle, dimColor: true, wrap: line.wrap, children: line.content }, `a-${i}`));
|
|
332
335
|
}
|
|
@@ -355,11 +358,16 @@ function MessageRow({ message, turnMeta, toolMeta, advisorDecision, foldReasonin
|
|
|
355
358
|
?? (message.display_metadata?.kind === 'agent' ? 'Agent' : undefined);
|
|
356
359
|
const isAdvisor = inferredToolName === 'Advisor';
|
|
357
360
|
const isAdvisorDecision = inferredToolName === 'AdvisorDecision';
|
|
361
|
+
if (isAdvisor)
|
|
362
|
+
lines.splice(0, lines.length, ...terminalText(advisorOpinionText(message.content)).split('\n'));
|
|
358
363
|
const visibility = toolHistoryVisibility(inferredToolName, message.content);
|
|
359
364
|
const previewCap = toolPreviewCap(inferredToolName);
|
|
360
365
|
const previewLines = lines.slice(0, previewCap);
|
|
361
366
|
const hidden = Math.max(0, lines.length - previewCap);
|
|
362
367
|
const shortId = message.tool_call_id.slice(-8);
|
|
368
|
+
if (message.display_metadata?.kind === 'advisor_checkpoint') {
|
|
369
|
+
return _jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsx(Text, { color: theme.subtle, wrap: "end", children: `已暂缓 · 顾问检查点 · 操作尚未执行(/show ${shortId} 查看详情,复核进展见后续记录)` }) });
|
|
370
|
+
}
|
|
363
371
|
// 头行升级:有 toolMeta(发起方反查命中)显示人话描述;Agent 子任务
|
|
364
372
|
// 再解析统计尾巴显示 ✓ N 轮 · M 次工具。无 meta(历史重载等)兜底旧格式。
|
|
365
373
|
const stats = inferredToolName === 'Agent' ? agentResultStats(message.content) : null;
|
|
@@ -377,12 +385,17 @@ function MessageRow({ message, turnMeta, toolMeta, advisorDecision, foldReasonin
|
|
|
377
385
|
: `[tool result · #${shortId}]`;
|
|
378
386
|
if (visibility === 'hidden')
|
|
379
387
|
return null;
|
|
388
|
+
if (isAdvisorDecision) {
|
|
389
|
+
return _jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsxs(Text, { color: theme.subtle, wrap: "end", children: [decisionMeta
|
|
390
|
+
? `顾问意见已处理 · ${advisorDecisionLabel(decisionMeta.decision)} · ${advisorEvidenceLabel(decisionMeta.verificationStatus)}`
|
|
391
|
+
: '顾问意见登记未完成 · 可重试', `(/show ${shortId} 查看详情)`] }) });
|
|
392
|
+
}
|
|
380
393
|
if (visibility === 'compact') {
|
|
381
394
|
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsx(Text, { color: theme.subtle, dimColor: true, wrap: "end", children: terminalText(headText) }) }));
|
|
382
395
|
}
|
|
383
396
|
return (_jsx(Box, { marginBottom: 1, paddingLeft: 2, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.subtle, ...(isAdvisor ? {} : { dimColor: true }), wrap: "end", children: terminalText(headText) }), advisorMeta && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.subtle, wrap: "end", children: terminalText(advisorMeta.status === 'completed'
|
|
384
|
-
?
|
|
385
|
-
:
|
|
397
|
+
? '顾问意见已返回,尚不代表事实核验通过'
|
|
398
|
+
: '顾问咨询未完成,已降级继续') }), advisorMeta.status === 'completed' && !advisorDecision && (_jsx(Text, { color: theme.subtle, wrap: "end", children: "\u987E\u95EE\u610F\u89C1\u5DF2\u8FD4\u56DE\uFF1B\u4E3B Agent \u7684\u6838\u9A8C\u4E0E\u91C7\u7EB3\u7ED3\u8BBA\u89C1\u540E\u7EED\u8BB0\u5F55\u3002" })), advisorDecision && (_jsx(Text, { color: theme.subtle, wrap: "end", children: terminalText(`${advisorDecision.decision} · ${advisorDecision.rationale}`) })), advisorDecision?.verificationPlan.map((item) => (_jsx(Text, { color: theme.subtle, wrap: "end", children: terminalText(`验证:${item}`) }, `advisor-card-verify-${item}`)))] })), decisionMeta && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.subtle, wrap: "end", children: terminalText(`关联咨询 ${decisionMeta.consultationId} · ${decisionMeta.rationale}`) }), decisionMeta.verificationPlan.map((item) => (_jsx(Text, { color: theme.subtle, wrap: "end", children: terminalText(`验证:${item}`) }, `advisor-verify-${item}`)))] })), previewLines.map((line, i) => (_jsx(Text, { ...(isAdvisor ? {} : { color: theme.subtle }), wrap: "end", children: line.length > 0 ? line : ' ' }, `p-${i}`))), hidden > 0 && (_jsx(Text, { color: theme.subtle, dimColor: true, children: `... ${figures.fold} 完整 ${lines.length} 行(输入 /show ${shortId} 查看)` }))] }) }));
|
|
386
399
|
}
|
|
387
400
|
}
|
|
388
401
|
}
|
package/src/ui/MessageList.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import React from 'react';
|
|
|
36
36
|
import { Box, Static, Text } from 'ink';
|
|
37
37
|
|
|
38
38
|
import { conciseToolDescription } from './toolProgress.js';
|
|
39
|
+
import { advisorOpinionText, advisorDecisionLabel, advisorEvidenceLabel } from '../advisor/presentation.js';
|
|
39
40
|
import { contentToDisplayText, contentToPlainText } from '../multimodal.js';
|
|
40
41
|
import type { AdvisorDecisionMetadata, Message } from '../types.js';
|
|
41
42
|
import { splitThinkBlocks } from '../utils/stripThink.js';
|
|
@@ -406,11 +407,14 @@ function MessageRow({
|
|
|
406
407
|
const ephemeral =
|
|
407
408
|
(message as { _ephemeral?: true })._ephemeral === true;
|
|
408
409
|
if (!ephemeral) return null;
|
|
409
|
-
const lines =
|
|
410
|
+
const lines = message.draftPresentation
|
|
411
|
+
? renderMarkdownLines(terminalText(message.content)).map(line => line.text)
|
|
412
|
+
: terminalText(message.content).split('\n');
|
|
410
413
|
const milestone = message._uiProgress === true;
|
|
411
414
|
return (
|
|
412
415
|
<Box marginBottom={1} paddingLeft={2}>
|
|
413
416
|
<Box flexDirection="column">
|
|
417
|
+
{message.draftPresentation && <Text color={theme.subtle}>{message.draftPresentation.label}</Text>}
|
|
414
418
|
{lines.map((line, i) => (
|
|
415
419
|
<Text key={`eph-${i}`} color={milestone ? theme.answer : theme.subtle} dimColor={!milestone} wrap="end">
|
|
416
420
|
{line}
|
|
@@ -500,6 +504,11 @@ function MessageRow({
|
|
|
500
504
|
第一条非空 prose 行 + 默认亮色正文(答案是主角)。代码块「零装饰」:
|
|
501
505
|
fence 行 dim、code 行顶格无前缀无染色逐字节原文 —— 复制粘贴即原文。
|
|
502
506
|
纯 <Static> 渲染(write-once),零刷屏风险,不碰 LiveArea 的 INV-1/INV-2。 */}
|
|
507
|
+
{answer.trim() && (message.presentation || tcCount > 0) && (
|
|
508
|
+
<Text color={theme.subtle} dimColor>
|
|
509
|
+
{message.presentation === 'final' ? '最终回答' : '工作进展 · 执行中说明'}
|
|
510
|
+
</Text>
|
|
511
|
+
)}
|
|
503
512
|
{buildAssistantDisplayLines(answer).map((line, i) => {
|
|
504
513
|
if (line.kind === 'fence') {
|
|
505
514
|
return (
|
|
@@ -557,11 +566,17 @@ function MessageRow({
|
|
|
557
566
|
?? (message.display_metadata?.kind === 'agent' ? 'Agent' : undefined);
|
|
558
567
|
const isAdvisor = inferredToolName === 'Advisor';
|
|
559
568
|
const isAdvisorDecision = inferredToolName === 'AdvisorDecision';
|
|
569
|
+
if (isAdvisor) lines.splice(0, lines.length, ...terminalText(advisorOpinionText(message.content)).split('\n'));
|
|
560
570
|
const visibility = toolHistoryVisibility(inferredToolName, message.content);
|
|
561
571
|
const previewCap = toolPreviewCap(inferredToolName);
|
|
562
572
|
const previewLines = lines.slice(0, previewCap);
|
|
563
573
|
const hidden = Math.max(0, lines.length - previewCap);
|
|
564
574
|
const shortId = message.tool_call_id.slice(-8);
|
|
575
|
+
if (message.display_metadata?.kind === 'advisor_checkpoint') {
|
|
576
|
+
return <Box marginBottom={1} paddingLeft={2}><Text color={theme.subtle} wrap="end">
|
|
577
|
+
{`已暂缓 · 顾问检查点 · 操作尚未执行(/show ${shortId} 查看详情,复核进展见后续记录)`}
|
|
578
|
+
</Text></Box>;
|
|
579
|
+
}
|
|
565
580
|
// 头行升级:有 toolMeta(发起方反查命中)显示人话描述;Agent 子任务
|
|
566
581
|
// 再解析统计尾巴显示 ✓ N 轮 · M 次工具。无 meta(历史重载等)兜底旧格式。
|
|
567
582
|
const stats =
|
|
@@ -579,6 +594,13 @@ function MessageRow({
|
|
|
579
594
|
? formatToolHead(toolMeta, shortId, statsTail)
|
|
580
595
|
: `[tool result · #${shortId}]`;
|
|
581
596
|
if (visibility === 'hidden') return null;
|
|
597
|
+
if (isAdvisorDecision) {
|
|
598
|
+
return <Box marginBottom={1} paddingLeft={2}><Text color={theme.subtle} wrap="end">
|
|
599
|
+
{decisionMeta
|
|
600
|
+
? `顾问意见已处理 · ${advisorDecisionLabel(decisionMeta.decision)} · ${advisorEvidenceLabel(decisionMeta.verificationStatus)}`
|
|
601
|
+
: '顾问意见登记未完成 · 可重试'}{`(/show ${shortId} 查看详情)`}
|
|
602
|
+
</Text></Box>;
|
|
603
|
+
}
|
|
582
604
|
if (visibility === 'compact') {
|
|
583
605
|
return (
|
|
584
606
|
<Box marginBottom={1} paddingLeft={2}>
|
|
@@ -598,8 +620,8 @@ function MessageRow({
|
|
|
598
620
|
<Box flexDirection="column">
|
|
599
621
|
<Text color={theme.subtle} wrap="end">
|
|
600
622
|
{terminalText(advisorMeta.status === 'completed'
|
|
601
|
-
?
|
|
602
|
-
:
|
|
623
|
+
? '顾问意见已返回,尚不代表事实核验通过'
|
|
624
|
+
: '顾问咨询未完成,已降级继续')}
|
|
603
625
|
</Text>
|
|
604
626
|
{advisorMeta.status === 'completed' && !advisorDecision && (
|
|
605
627
|
<Text color={theme.subtle} wrap="end">顾问意见已返回;主 Agent 的核验与采纳结论见后续记录。</Text>
|
package/src/ui/draftNotice.js
CHANGED
|
@@ -1,8 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
switch (reason) {
|
|
3
|
-
case 'advisor_checkpoint': return '交付前复核:草稿尚未交付,接下来咨询顾问,再由主 Agent 核验并定稿。';
|
|
4
|
-
case 'formal_delivery': return '整理交付:草稿尚未交付,接下来核对委派结果或补齐正式成果提交。';
|
|
5
|
-
case 'interactive_question': return '等待关键决策:刚才的提问草稿将改为交互问题,请在输入区作答。';
|
|
6
|
-
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
export { answerPreparationNotice as draftNotice } from '../runtime/answerPreparation.js';
|
package/src/ui/draftNotice.ts
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function draftNotice(reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason']): string {
|
|
4
|
-
switch (reason) {
|
|
5
|
-
case 'advisor_checkpoint': return '交付前复核:草稿尚未交付,接下来咨询顾问,再由主 Agent 核验并定稿。';
|
|
6
|
-
case 'formal_delivery': return '整理交付:草稿尚未交付,接下来核对委派结果或补齐正式成果提交。';
|
|
7
|
-
case 'interactive_question': return '等待关键决策:刚才的提问草稿将改为交互问题,请在输入区作答。';
|
|
8
|
-
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
9
|
-
}
|
|
10
|
-
}
|
|
1
|
+
export { answerPreparationNotice as draftNotice } from '../runtime/answerPreparation.js';
|
package/src/ui/hooks/useChat.js
CHANGED
|
@@ -1055,6 +1055,13 @@ export function handleEvent(ev, setters) {
|
|
|
1055
1055
|
// V2 工作流发结构化 `workflow_progress`;旧插件仍可发 `plugin_progress`。
|
|
1056
1056
|
// 收口到 LoopEvent:未识别的 PluginEvent type 走 default no-op
|
|
1057
1057
|
const e = ev;
|
|
1058
|
+
if (e.type === 'turn_done' || e.type === 'interrupted' || (e.type === 'error' && !isRecoverableLoopError(e))) {
|
|
1059
|
+
setters.cancelStreamingFlush();
|
|
1060
|
+
setters.setStreamingText(() => '');
|
|
1061
|
+
setters.cancelReasoningFlush();
|
|
1062
|
+
setters.setReasoningPreview(() => '');
|
|
1063
|
+
setters.setProgressStage('idle');
|
|
1064
|
+
}
|
|
1058
1065
|
if (['assistant_message', 'tool_start', 'turn_done', 'interrupted', 'error'].includes(e.type))
|
|
1059
1066
|
setters.setProgressNotice?.(null);
|
|
1060
1067
|
switch (e.type) {
|
|
@@ -1067,6 +1074,8 @@ export function handleEvent(ev, setters) {
|
|
|
1067
1074
|
setters.appendStreamingDelta(e.delta);
|
|
1068
1075
|
break;
|
|
1069
1076
|
case 'assistant_message':
|
|
1077
|
+
if (e.message.presentation === 'final')
|
|
1078
|
+
setters.setPluginProgress(null);
|
|
1070
1079
|
// history 已被 runQuery 直接 push。先 flush 残留 streaming buffer
|
|
1071
1080
|
// 把末段 text 落到 streamingText state(avoid 视觉跳跃),再清空
|
|
1072
1081
|
// streamingText —— Static 已 commit 完整 assistant message 接管显示。
|
|
@@ -1085,13 +1094,26 @@ export function handleEvent(ev, setters) {
|
|
|
1085
1094
|
setters.setTransientError?.(null);
|
|
1086
1095
|
setters.bump();
|
|
1087
1096
|
break;
|
|
1097
|
+
case 'injected_message':
|
|
1098
|
+
// Milestones preserve live previews; legacy drafts transfer them to history.
|
|
1099
|
+
if (e.message.role === 'system' && e.message._uiProgress) {
|
|
1100
|
+
if (e.message.draftPresentation) {
|
|
1101
|
+
setters.flushStreamingBuffer();
|
|
1102
|
+
setters.setStreamingText(() => '');
|
|
1103
|
+
setters.cancelReasoningFlush();
|
|
1104
|
+
setters.setReasoningPreview(() => '');
|
|
1105
|
+
}
|
|
1106
|
+
setters.bump();
|
|
1107
|
+
}
|
|
1108
|
+
break;
|
|
1088
1109
|
case 'assistant_draft_discarded':
|
|
1089
1110
|
setters.flushStreamingBuffer();
|
|
1090
1111
|
setters.setStreamingText(() => '');
|
|
1091
1112
|
setters.cancelReasoningFlush();
|
|
1092
1113
|
setters.setReasoningPreview(() => '');
|
|
1093
1114
|
setters.setProgressStage('thinking');
|
|
1094
|
-
|
|
1115
|
+
if (e.textLength > 0)
|
|
1116
|
+
setters.emitProgressLine?.(draftNotice(e.reason));
|
|
1095
1117
|
setters.setProgressNotice?.(draftNotice(e.reason));
|
|
1096
1118
|
break;
|
|
1097
1119
|
case 'tool_start':
|
|
@@ -1177,6 +1199,8 @@ export function handleEvent(ev, setters) {
|
|
|
1177
1199
|
// Diagnostics are intentionally local-safe and currently consumed by CLI/stream hosts.
|
|
1178
1200
|
break;
|
|
1179
1201
|
case 'turn_done':
|
|
1202
|
+
if (e.completionStatus === 'no_answer')
|
|
1203
|
+
setters.setError(e.warning ?? '本轮未生成最终答复,可以重试。');
|
|
1180
1204
|
setters.setProgressStage('idle');
|
|
1181
1205
|
setters.setTransientError?.(null);
|
|
1182
1206
|
setters.bump();
|
package/src/ui/hooks/useChat.ts
CHANGED
|
@@ -1466,6 +1466,13 @@ export function handleEvent(
|
|
|
1466
1466
|
|
|
1467
1467
|
// 收口到 LoopEvent:未识别的 PluginEvent type 走 default no-op
|
|
1468
1468
|
const e = ev as LoopEvent;
|
|
1469
|
+
if (e.type === 'turn_done' || e.type === 'interrupted' || (e.type === 'error' && !isRecoverableLoopError(e))) {
|
|
1470
|
+
setters.cancelStreamingFlush();
|
|
1471
|
+
setters.setStreamingText(() => '');
|
|
1472
|
+
setters.cancelReasoningFlush();
|
|
1473
|
+
setters.setReasoningPreview(() => '');
|
|
1474
|
+
setters.setProgressStage('idle');
|
|
1475
|
+
}
|
|
1469
1476
|
if (['assistant_message', 'tool_start', 'turn_done', 'interrupted', 'error'].includes(e.type)) setters.setProgressNotice?.(null);
|
|
1470
1477
|
switch (e.type) {
|
|
1471
1478
|
case 'user_message_committed':
|
|
@@ -1477,6 +1484,7 @@ export function handleEvent(
|
|
|
1477
1484
|
setters.appendStreamingDelta(e.delta);
|
|
1478
1485
|
break;
|
|
1479
1486
|
case 'assistant_message':
|
|
1487
|
+
if (e.message.presentation === 'final') setters.setPluginProgress(null);
|
|
1480
1488
|
// history 已被 runQuery 直接 push。先 flush 残留 streaming buffer
|
|
1481
1489
|
// 把末段 text 落到 streamingText state(avoid 视觉跳跃),再清空
|
|
1482
1490
|
// streamingText —— Static 已 commit 完整 assistant message 接管显示。
|
|
@@ -1495,13 +1503,25 @@ export function handleEvent(
|
|
|
1495
1503
|
setters.setTransientError?.(null);
|
|
1496
1504
|
setters.bump();
|
|
1497
1505
|
break;
|
|
1506
|
+
case 'injected_message':
|
|
1507
|
+
// Milestones preserve live previews; legacy drafts transfer them to history.
|
|
1508
|
+
if (e.message.role === 'system' && e.message._uiProgress) {
|
|
1509
|
+
if (e.message.draftPresentation) {
|
|
1510
|
+
setters.flushStreamingBuffer();
|
|
1511
|
+
setters.setStreamingText(() => '');
|
|
1512
|
+
setters.cancelReasoningFlush();
|
|
1513
|
+
setters.setReasoningPreview(() => '');
|
|
1514
|
+
}
|
|
1515
|
+
setters.bump();
|
|
1516
|
+
}
|
|
1517
|
+
break;
|
|
1498
1518
|
case 'assistant_draft_discarded':
|
|
1499
1519
|
setters.flushStreamingBuffer();
|
|
1500
1520
|
setters.setStreamingText(() => '');
|
|
1501
1521
|
setters.cancelReasoningFlush();
|
|
1502
1522
|
setters.setReasoningPreview(() => '');
|
|
1503
1523
|
setters.setProgressStage('thinking');
|
|
1504
|
-
setters.emitProgressLine?.(draftNotice(e.reason));
|
|
1524
|
+
if (e.textLength > 0) setters.emitProgressLine?.(draftNotice(e.reason));
|
|
1505
1525
|
setters.setProgressNotice?.(draftNotice(e.reason));
|
|
1506
1526
|
break;
|
|
1507
1527
|
case 'tool_start':
|
|
@@ -1585,6 +1605,7 @@ export function handleEvent(
|
|
|
1585
1605
|
// Diagnostics are intentionally local-safe and currently consumed by CLI/stream hosts.
|
|
1586
1606
|
break;
|
|
1587
1607
|
case 'turn_done':
|
|
1608
|
+
if (e.completionStatus === 'no_answer') setters.setError(e.warning ?? '本轮未生成最终答复,可以重试。');
|
|
1588
1609
|
setters.setProgressStage('idle');
|
|
1589
1610
|
setters.setTransientError?.(null);
|
|
1590
1611
|
setters.bump();
|