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.
Files changed (84) hide show
  1. package/package.json +1 -1
  2. package/plugins/workflow-runner/src/runtime/context.js +5 -17
  3. package/plugins/workflow-runner/src/runtime/context.ts +4 -15
  4. package/plugins/workflow-runner/src/runtime/model.js +4 -2
  5. package/plugins/workflow-runner/src/runtime/model.ts +3 -2
  6. package/plugins/workflow-runner/test/api.test.ts +4 -2
  7. package/plugins/workflow-runner/test/embeddedExecution.test.ts +5 -6
  8. package/plugins/workflow-runner/test/modelOutcome.test.ts +36 -0
  9. package/skills/dynamic-ui/SKILL.md +38 -278
  10. package/skills/dynamic-ui/mermaid.md +32 -0
  11. package/skills/dynamic-ui/scenes/architecture-and-flow.md +4 -1
  12. package/skills/dynamic-ui/scenes/data-visualization.md +5 -2
  13. package/skills/dynamic-ui/svg-design.md +251 -0
  14. package/skills/dynamic-ui/templates/README.md +2 -1
  15. package/skills/dynamic-ui/tokens/visual-tokens.md +4 -5
  16. package/src/advisor/README.md +24 -0
  17. package/src/advisor/advisorCoordinator.js +101 -45
  18. package/src/advisor/advisorCoordinator.ts +90 -51
  19. package/src/advisor/deliveryReview.js +35 -0
  20. package/src/advisor/deliveryReview.ts +30 -0
  21. package/src/advisor/presentation.js +10 -0
  22. package/src/advisor/presentation.ts +12 -0
  23. package/src/advisor/reviewContext.js +60 -0
  24. package/src/advisor/reviewContext.ts +72 -0
  25. package/src/context/persistContext.js +5 -1
  26. package/src/context/persistContext.ts +5 -2
  27. package/src/context/tokenCounter.js +3 -0
  28. package/src/context/tokenCounter.ts +2 -0
  29. package/src/llm/messageProvenance.js +1 -1
  30. package/src/llm/messageProvenance.ts +1 -1
  31. package/src/loop.js +226 -31
  32. package/src/loop.ts +205 -31
  33. package/src/memory/fileStore.js +2 -34
  34. package/src/memory/fileStore.ts +2 -26
  35. package/src/prompts/policyRegistry.js +13 -9
  36. package/src/prompts/policyRegistry.ts +15 -9
  37. package/src/prompts/promptAssembly.js +16 -23
  38. package/src/prompts/promptAssembly.ts +22 -27
  39. package/src/prompts/visualPresentation.js +28 -0
  40. package/src/prompts/visualPresentation.ts +26 -0
  41. package/src/runtime/agentRuntimeContext.js +3 -0
  42. package/src/runtime/agentRuntimeContext.ts +4 -0
  43. package/src/runtime/answerPreparation.js +9 -0
  44. package/src/runtime/answerPreparation.ts +11 -0
  45. package/src/runtime/eventSemantics.js +2 -0
  46. package/src/runtime/eventSemantics.ts +2 -0
  47. package/src/runtime/fileLock.js +75 -0
  48. package/src/runtime/fileLock.ts +73 -0
  49. package/src/runtime/finalAnswer.js +9 -0
  50. package/src/runtime/finalAnswer.ts +13 -0
  51. package/src/runtime/lifecycleTrace.js +16 -0
  52. package/src/runtime/lifecycleTrace.ts +22 -0
  53. package/src/runtime/outputContract.js +17 -0
  54. package/src/runtime/outputContract.ts +34 -0
  55. package/src/session/runAgentTurn.js +7 -2
  56. package/src/session/runAgentTurn.ts +7 -2
  57. package/src/session/runTurn.js +15 -1
  58. package/src/session/runTurn.ts +13 -1
  59. package/src/session/runtimeStore.js +11 -28
  60. package/src/session/runtimeStore.ts +9 -23
  61. package/src/tools/advisor/advisor.js +56 -40
  62. package/src/tools/advisor/advisor.ts +57 -39
  63. package/src/tools/advisor/advisorDecision.js +6 -3
  64. package/src/tools/advisor/advisorDecision.ts +6 -3
  65. package/src/tools/agent/agent.js +14 -6
  66. package/src/tools/agent/agent.ts +14 -5
  67. package/src/tools/bash/semantics.js +91 -26
  68. package/src/tools/bash/semantics.ts +63 -33
  69. package/src/tools/index.js +4 -1
  70. package/src/tools/index.ts +4 -1
  71. package/src/tools/skill/searchSkills.js +2 -2
  72. package/src/tools/skill/searchSkills.ts +2 -2
  73. package/src/tools/skill/skill.js +4 -0
  74. package/src/tools/skill/skill.ts +3 -0
  75. package/src/types.ts +19 -6
  76. package/src/ui/LiveArea.js +2 -1
  77. package/src/ui/LiveArea.tsx +2 -1
  78. package/src/ui/MessageList.js +18 -5
  79. package/src/ui/MessageList.tsx +25 -3
  80. package/src/ui/draftNotice.js +1 -8
  81. package/src/ui/draftNotice.ts +1 -10
  82. package/src/ui/hooks/useChat.js +25 -1
  83. package/src/ui/hooks/useChat.ts +22 -1
  84. 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
- // Classification only, never an execution restriction. Unknown shell syntax
28
- // retains the normal execution semantics rather than hiding side effects.
29
- if (/[><`]|\$\(|\$\{|\btee\b|(?<!&)&(?!&)/i.test(command)) return false;
30
- const body = command.replace(
31
- /^(?:bash|sh|zsh|dash)\s+-c\s+(['"])([\s\S]*)\1\s*$/,
32
- '$2',
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
- while (index < tokens.length && tokens[index] === 'sudo') index++;
47
- while (
48
- index < tokens.length &&
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|npm|npx|tsc|python|python3)$/.test(name) &&
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
- if (/[><`]|\$\(|\$\{|\btee\b|(?<!&)&(?!&)/i.test(command)) return false;
188
- const body = command.replace(
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 segment of segments) {
198
- if (isReadOnlyShellCommand(segment)) continue;
199
- const tokens = segment.split(/\s+/).filter(Boolean);
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 (
@@ -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
- return ` - ${where}:${i.message}\n 收到的值:${seen(i.path)}`;
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 {
@@ -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
- return ` - ${where}:${i.message}\n 收到的值:${seen(i.path)}`;
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: '这是已安装目录,不表示依赖工具已启用。同名项用 sourceId 精确加载;未找到可换关键词,仅加载当前阶段相关技能。',
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: '这是已安装目录,不表示依赖工具已启用。同名项用 sourceId 精确加载;未找到可换关键词,仅加载当前阶段相关技能。',
61
+ note: '宿主提供有效工具集合时已按依赖筛选;目录不授予执行权限。同名项用 sourceId 精确加载,仅加载当前阶段相关技能。',
62
62
  }),
63
63
  };
64
64
  },
@@ -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);
@@ -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 —— 已发请求,等首个 token(reasoning 或 text 或 tool_call)
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
- * 消费方应清掉 live 草稿;该草稿不会写入会话 history。
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'
@@ -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
  }
@@ -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) {
@@ -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 = terminalText(message.content).split('\n');
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: _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: milestone ? theme.answer : theme.subtle, dimColor: !milestone, wrap: "end", children: line }, `eph-${i}`))) }) }));
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
- ? `咨询 ${advisorMeta.consultationId} · 版本 ${advisorMeta.taskVersion}/${advisorMeta.workVersion}/${advisorMeta.evidenceVersion}`
385
- : `咨询 ${advisorMeta.consultationId} 未完成,检查点已降级放行`) }), 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} 查看)` }))] }) }));
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
  }
@@ -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 = terminalText(message.content).split('\n');
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
- ? `咨询 ${advisorMeta.consultationId} · 版本 ${advisorMeta.taskVersion}/${advisorMeta.workVersion}/${advisorMeta.evidenceVersion}`
602
- : `咨询 ${advisorMeta.consultationId} 未完成,检查点已降级放行`)}
623
+ ? '顾问意见已返回,尚不代表事实核验通过'
624
+ : '顾问咨询未完成,已降级继续')}
603
625
  </Text>
604
626
  {advisorMeta.status === 'completed' && !advisorDecision && (
605
627
  <Text color={theme.subtle} wrap="end">顾问意见已返回;主 Agent 的核验与采纳结论见后续记录。</Text>
@@ -1,8 +1 @@
1
- export function draftNotice(reason) {
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';
@@ -1,10 +1 @@
1
- import type { LoopEvent } from '../types.js';
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';
@@ -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
- setters.emitProgressLine?.(draftNotice(e.reason));
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();
@@ -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();