minimal-agent 0.18.6 → 0.18.7

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 (61) 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/tokenCounter.js +3 -0
  26. package/src/context/tokenCounter.ts +2 -0
  27. package/src/llm/messageProvenance.js +1 -1
  28. package/src/llm/messageProvenance.ts +1 -1
  29. package/src/loop.js +203 -30
  30. package/src/loop.ts +183 -30
  31. package/src/prompts/policyRegistry.js +17 -9
  32. package/src/prompts/policyRegistry.ts +17 -9
  33. package/src/runtime/answerPreparation.js +9 -0
  34. package/src/runtime/answerPreparation.ts +11 -0
  35. package/src/runtime/eventSemantics.js +2 -0
  36. package/src/runtime/eventSemantics.ts +2 -0
  37. package/src/runtime/finalAnswer.js +7 -0
  38. package/src/runtime/finalAnswer.ts +9 -0
  39. package/src/runtime/lifecycleTrace.js +16 -0
  40. package/src/runtime/lifecycleTrace.ts +22 -0
  41. package/src/session/runTurn.js +15 -1
  42. package/src/session/runTurn.ts +13 -1
  43. package/src/tools/advisor/advisor.js +56 -40
  44. package/src/tools/advisor/advisor.ts +57 -39
  45. package/src/tools/advisor/advisorDecision.js +6 -3
  46. package/src/tools/advisor/advisorDecision.ts +6 -3
  47. package/src/tools/agent/agent.js +7 -1
  48. package/src/tools/agent/agent.ts +7 -1
  49. package/src/tools/bash/semantics.js +91 -26
  50. package/src/tools/bash/semantics.ts +63 -33
  51. package/src/tools/index.js +4 -1
  52. package/src/tools/index.ts +4 -1
  53. package/src/types.ts +15 -6
  54. package/src/ui/LiveArea.js +2 -1
  55. package/src/ui/LiveArea.tsx +2 -1
  56. package/src/ui/MessageList.js +18 -5
  57. package/src/ui/MessageList.tsx +25 -3
  58. package/src/ui/draftNotice.js +1 -8
  59. package/src/ui/draftNotice.ts +1 -10
  60. package/src/ui/hooks/useChat.js +25 -1
  61. package/src/ui/hooks/useChat.ts +22 -1
@@ -12,31 +12,100 @@
12
12
  * 纯函数模块,无 I/O,便于单测。
13
13
  * ============================================================
14
14
  */
15
+ /** Small conservative lexer, not a shell evaluator. Quoted regex operators are
16
+ * data. Expansions, redirects and unsupported syntax remain non-read-only. */
17
+ function inspectionCommands(command) {
18
+ const commands = [];
19
+ let words = [];
20
+ let word = '';
21
+ let started = false;
22
+ let quote = '';
23
+ const flushWord = () => { if (started)
24
+ words.push(word); word = ''; started = false; };
25
+ const flushCommand = () => { flushWord(); if (words.length)
26
+ commands.push(words); words = []; };
27
+ for (let i = 0; i < command.length; i++) {
28
+ const c = command[i];
29
+ if (quote === "'") {
30
+ if (c === "'")
31
+ quote = '';
32
+ else
33
+ word += c;
34
+ continue;
35
+ }
36
+ if (c === '\\') {
37
+ const next = command[++i];
38
+ if (next === undefined)
39
+ return null;
40
+ if (quote === '"' && !['$', '`', '"', '\\', '\n'].includes(next))
41
+ word += '\\';
42
+ if (next !== '\n') {
43
+ word += next;
44
+ started = true;
45
+ }
46
+ continue;
47
+ }
48
+ if (c === '`' || (c === '$' && /[({\w?'"*#@!$-]/.test(command[i + 1] ?? '')))
49
+ return null;
50
+ if (quote === '"') {
51
+ if (c === '"')
52
+ quote = '';
53
+ else
54
+ word += c;
55
+ continue;
56
+ }
57
+ if (c === "'" || c === '"') {
58
+ quote = c;
59
+ started = true;
60
+ continue;
61
+ }
62
+ if (/[<>(){}]/.test(c))
63
+ return null;
64
+ if (c === '#' && !started) {
65
+ while (i + 1 < command.length && command[i + 1] !== '\n')
66
+ i++;
67
+ continue;
68
+ }
69
+ if (c === '&' && command[i + 1] !== '&')
70
+ return null;
71
+ if (c === '&' || c === '|' || c === ';' || c === '\n') {
72
+ flushCommand();
73
+ if ((c === '&' || c === '|') && command[i + 1] === c)
74
+ i++;
75
+ continue;
76
+ }
77
+ if (/\s/.test(c)) {
78
+ flushWord();
79
+ continue;
80
+ }
81
+ word += c;
82
+ started = true;
83
+ }
84
+ if (quote)
85
+ return null;
86
+ flushCommand();
87
+ return commands;
88
+ }
15
89
  /** Conservative shell side-effect classification used by Advisor gates. */
16
90
  export function isReadOnlyShellCommand(command) {
17
91
  if (!command.trim())
18
92
  return true;
19
- // Classification only, never an execution restriction. Unknown shell syntax
20
- // retains the normal execution semantics rather than hiding side effects.
21
- if (/[><`]|\$\(|\$\{|\btee\b|(?<!&)&(?!&)/i.test(command))
93
+ // A conservative scheduling hint, not an OS sandbox. Strict read-only
94
+ // consumers must reject shell execution rather than trust this heuristic.
95
+ // Do not strip nested shell quoting: expansion may happen in the outer shell.
96
+ const segments = inspectionCommands(command);
97
+ if (!segments)
22
98
  return false;
23
- const body = command.replace(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])([\s\S]*)\1\s*$/, '$2');
24
- const segments = body
25
- .split(/&&|\|\||[;|\n]/)
26
- .map((part) => part.trim())
27
- .filter(Boolean);
28
- for (const segment of segments) {
29
- const tokens = segment.split(/\s+/).filter(Boolean);
99
+ for (const tokens of segments) {
30
100
  let index = 0;
31
101
  if (tokens[index] === 'command' &&
32
102
  /^(?:-v|--verbose|-V)$/.test(tokens[index + 1] ?? ''))
33
103
  continue;
34
- while (index < tokens.length && tokens[index] === 'sudo')
35
- index++;
36
- while (index < tokens.length &&
37
- /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index]))
38
- index++;
39
- const name = tokens[index]?.replace(/^.*\//, '').toLowerCase();
104
+ if (tokens[index] === 'sudo' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index] ?? ''))
105
+ return false;
106
+ if (/[\\/]/.test(tokens[index] ?? ''))
107
+ return false;
108
+ const name = tokens[index]?.toLowerCase();
40
109
  if (!name ||
41
110
  name === 'cd' ||
42
111
  name === ':' ||
@@ -112,7 +181,7 @@ export function isReadOnlyShellCommand(command) {
112
181
  continue;
113
182
  if ((name === 'env' || name === 'date') && tokens.length === index + 1)
114
183
  continue;
115
- if (/^(?:node|bun|npm|npx|tsc|python|python3)$/.test(name) &&
184
+ if (/^(?:node|bun|python|python3)$/.test(name) &&
116
185
  tokens.length === index + 2 &&
117
186
  /^(?:--version|-v|--help|-h)$/.test(tokens[index + 1]))
118
187
  continue;
@@ -137,19 +206,15 @@ export function isReadOnlyShellCommand(command) {
137
206
  export function isVerificationShellCommand(command) {
138
207
  if (!command.trim())
139
208
  return true;
140
- if (/[><`]|\$\(|\$\{|\btee\b|(?<!&)&(?!&)/i.test(command))
209
+ const segments = inspectionCommands(command);
210
+ if (!segments)
141
211
  return false;
142
- const body = command.replace(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])([\s\S]*)\1\s*$/, '$2');
143
- const segments = body
144
- .split(/&&|\|\||[;|\n]/)
145
- .map((part) => part.trim())
146
- .filter(Boolean);
147
212
  if (!segments.length)
148
213
  return false;
149
- for (const segment of segments) {
150
- if (isReadOnlyShellCommand(segment))
214
+ for (const tokens of segments) {
215
+ // Re-quote tokens for the shared classifier without reinterpreting data.
216
+ if (isReadOnlyShellCommand(tokens.map(token => `'${token.replace(/'/g, `'\\''`)}'`).join(' ')))
151
217
  continue;
152
- const tokens = segment.split(/\s+/).filter(Boolean);
153
218
  let index = 0;
154
219
  while (index < tokens.length && tokens[index] === 'sudo')
155
220
  index++;
@@ -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 {
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 协议) */
@@ -593,6 +597,8 @@ export interface ToolCallContext {
593
597
  advisorInvocation?: 'agent' | 'manual';
594
598
  /** Advisor 协调器运行时桥;只存在于主循环,不暴露给模型或持久化。 */
595
599
  advisorRuntime?: AdvisorRuntimeBridge;
600
+ /** Live host context used by the main request, not tool-provided instructions. */
601
+ advisorReviewContext?: () => import('./advisor/reviewContext.js').AdvisorReviewContext;
596
602
  /**
597
603
  * 主循环使用的 provider —— Agent 等「工具内部再起子查询」的派生工具用它
598
604
  * 跑嵌套 runQuery(子 agent 与主 agent 同一模型,不另配凭据)。
@@ -1122,8 +1128,8 @@ export type LlmStreamEvent =
1122
1128
  * 工作过程显示阶段标签(UI 的 LiveArea 据此选择文字 / 决定是否隐藏整面板)。
1123
1129
  *
1124
1130
  * idle —— 当前没有任何工作(隐藏 LiveArea)
1125
- * thinking —— 已发请求,等首个 token(reasoning 或 text 或 tool_call)
1126
- * streaming —— 模型在流式输出最终回答文本
1131
+ * thinking —— 正在准备候选答案或等待工具调用,候选正文不公开
1132
+ * streaming —— 内部检查结束后,最终表达请求的真实正文流
1127
1133
  * tool_executing —— 至少一个工具在跑(并行时栈式显示多个)
1128
1134
  * compacting —— 正在跑自动 / reactive / 手动压缩
1129
1135
  */
@@ -1221,8 +1227,9 @@ export type LoopEvent =
1221
1227
  message: Extract<Message, { role: 'assistant' }>;
1222
1228
  }
1223
1229
  /**
1224
- * 模型已经流出了一份正文/思考草稿,但宿主确定它还不能成为本轮回答。
1225
- * 消费方应清掉 live 草稿;该草稿不会写入会话 history。
1230
+ * 内部候选正文尚不能交付,需要进入检查或补全阶段。
1231
+ * 新内核候选正文不广播;textLength=0,仅表示准备/复核阶段变化。
1232
+ * textLength>0 仅供旧事件兼容,不应作为正常展示路径。
1226
1233
  */
1227
1234
  | {
1228
1235
  type: 'assistant_draft_discarded';
@@ -1315,7 +1322,8 @@ export type LoopEvent =
1315
1322
  | 'completed'
1316
1323
  | 'delivery_incomplete'
1317
1324
  | 'delivery_unverified'
1318
- | 'claims_unverified';
1325
+ | 'claims_unverified'
1326
+ | 'no_answer';
1319
1327
  warning?: string;
1320
1328
  /** 本回合的资源账本。见 TurnResourceUsage —— 消费方务必先看 `trustworthy`。 */
1321
1329
  usage?: TurnResourceUsage;
@@ -1336,7 +1344,7 @@ export type LoopEvent =
1336
1344
  * - reason: 'manual' 用户按ESC/Ctrl+C手动中断
1337
1345
  * - reason: 'queued_input' 有新的排队输入需要在安全点处理
1338
1346
  */
1339
- | { type: 'interrupted'; reason?: 'manual' | 'queued_input' }
1347
+ | { type: 'interrupted'; reason?: 'manual' | 'queued_input'; usage?: TurnResourceUsage }
1340
1348
  /**
1341
1349
  * 出错(API 报错 / 网络异常等)。
1342
1350
  *
@@ -1357,6 +1365,7 @@ export type LoopEvent =
1357
1365
  | {
1358
1366
  type: 'error';
1359
1367
  error: string;
1368
+ usage?: TurnResourceUsage;
1360
1369
  code?:
1361
1370
  | 'llm_error'
1362
1371
  | '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';