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.
- 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/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 +203 -30
- package/src/loop.ts +183 -30
- package/src/prompts/policyRegistry.js +17 -9
- package/src/prompts/policyRegistry.ts +17 -9
- 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/finalAnswer.js +7 -0
- package/src/runtime/finalAnswer.ts +9 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- package/src/session/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- 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 +7 -1
- package/src/tools/agent/agent.ts +7 -1
- 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/types.ts +15 -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
|
@@ -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
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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|
|
|
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
|
-
|
|
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
|
|
150
|
-
|
|
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
|
-
//
|
|
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 {
|
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 ——
|
|
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
|
-
*
|
|
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'
|
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';
|