minimal-agent 0.5.3 → 0.5.4
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/src/loop.js +3 -0
- package/src/prompts/system.js +11 -0
- package/src/ui/App.js +2 -2
- package/src/ui/MessageList.js +4 -2
- package/src/ui/hooks/useChat.js +64 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "最小化 Agent 系统 —— 10 工具 + 插件系统 + workflow DSL + 自动压缩 + OpenAI 兼容 + Ink TUI;NodeNext + tsc 原地编译,dev 用 Bun .ts、install 用 Node .js(学习/教学用)",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
package/src/loop.js
CHANGED
|
@@ -107,6 +107,9 @@ export async function* runQuery(userInput, options) {
|
|
|
107
107
|
else if (ev.field === 'reasoning_details' && ev.items) {
|
|
108
108
|
reasoningDetails.push(...ev.items);
|
|
109
109
|
}
|
|
110
|
+
if (ev.delta) {
|
|
111
|
+
yield { type: 'reasoning_delta', field: ev.field, delta: ev.delta };
|
|
112
|
+
}
|
|
110
113
|
}
|
|
111
114
|
// ev.type === 'done' 时无需处理:循环自然结束
|
|
112
115
|
}
|
package/src/prompts/system.js
CHANGED
|
@@ -34,6 +34,17 @@ export async function getSystemPrompt(cwd, tools) {
|
|
|
34
34
|
|
|
35
35
|
# 思维与立场(核心:像专家一样独立思考)
|
|
36
36
|
- **你是工程协作者,不是奉承者**。不无条件夸赞、不为附和而附和、不同意时直说。不用"很好的问题"、"你说得对"、"完全同意"这类前缀——直接答内容。
|
|
37
|
+
- **先判断用户意图再行动**——这是最重要的第一步,区分以下三种情况:
|
|
38
|
+
- ✅ **明确执行(直接动手,不要犹豫)**:用户表述包含明确的行动指令。
|
|
39
|
+
动词特征:「修改/改/调整/修复/优化/更新/替换/删/移除/写/创建/实现/做/生成/新建/开发/执行/跑/运行/提交/部署/安装」
|
|
40
|
+
确认性指令也算:「对就这么改」「是的按这个方案实现」「好的麻烦改一下」
|
|
41
|
+
- ❌ **纯讨论(绝对不能动文件,只回答)**:用户只是提问、咨询、探讨、评估。
|
|
42
|
+
特征:问号结尾、「是不是/怎么样/为什么/好不好/能否/你觉得」、纯观点表达「我觉得...」「这里好像有...」
|
|
43
|
+
即使你发现明显问题,也只能给文字建议,**禁止调用 Edit/Write/MultiEdit/Bash 写操作等任何修改类工具**
|
|
44
|
+
- ❓ **模糊场景(仅一次确认,不要啰嗦)**:介于两者之间无法判断时。
|
|
45
|
+
例:「这个接口响应太慢了」「这个正则好像有问题」
|
|
46
|
+
只问一句:「你是需要我实际修改/修复,还是只是讨论这个问题?」—— 用户说改就立刻改,说讨论就停
|
|
47
|
+
- **兜底原则**:宁可多问一次也不要擅自修改;但只要用户明确说要改,必须立刻执行,不要二次确认。
|
|
37
48
|
- **基于证据,不基于揣测用户想听什么**。没读过的代码先 Read,没验证过的行为先验证,不凭印象答。回答前先问自己:这个判断有具体证据吗,还是我在猜?
|
|
38
49
|
- **不知道就说不知道**。明确边界:"这个版本号我不确定"、"这个 API 我没看过文档"、"这部分行为我没验证过"。胡编一个看起来对的答案,代价远高于承认不知道。
|
|
39
50
|
- **明示 confidence 分层**。结论按确定度分层表达——"已 verify / 高度确信"、"基于代码合理推测"、"未验证猜测"——让用户基于你的不确定程度决策,不要把推测说成事实。
|
package/src/ui/App.js
CHANGED
|
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* ============================================================
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { Box, Text } from 'ink';
|
|
11
|
+
import { Box, Text, Static } from 'ink';
|
|
12
12
|
import { saveContext } from '../context/persistContext.js';
|
|
13
13
|
import { InputBox } from './InputBox.js';
|
|
14
14
|
import { MessageList } from './MessageList.js';
|
|
@@ -33,5 +33,5 @@ export function App({ provider, initialHistory }) {
|
|
|
33
33
|
await chat.clearHistory();
|
|
34
34
|
process.stdout.write(CLEAR_SCREEN);
|
|
35
35
|
}, [chat]);
|
|
36
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "minimal-agent" }), _jsx(Text, { color: "gray", children: ` · ${provider.name}/${provider.model} · /new 清空 · /compact 压缩 · /exit 退出 · Ctrl+C 中断` })] }), _jsx(MessageList, { history: chat.history, streamingText: chat.streamingText }), _jsx(ToolStatus, { status: chat.toolStatus, compacting: chat.compacting }), chat.error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["\u26A0 ", chat.error] }) })), chat.interrupted && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A1 \u64CD\u4F5C\u88AB\u7528\u6237\u4E2D\u65AD\uFF0C\u7B49\u5F85\u65B0\u7684\u4EFB\u52A1\u8F93\u5165..." }) })), _jsx(InputBox, { onSubmit: chat.submit, disabled: chat.isLoading, onAbort: chat.abort, onClear: handleClear, onCompact: chat.compactNow }), _jsx(StatusLine, { provider: provider, history: chat.history, pluginProgress: chat.pluginProgress })] }));
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [null], children: () => (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "minimal-agent" }), _jsx(Text, { color: "gray", children: ` · ${provider.name}/${provider.model} · /new 清空 · /compact 压缩 · /exit 退出 · Ctrl+C 中断` })] })) }), _jsx(MessageList, { history: chat.history, streamingText: chat.streamingText, streamingReasoning: chat.streamingReasoning }), _jsx(ToolStatus, { status: chat.toolStatus, compacting: chat.compacting }), chat.error && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", children: ["\u26A0 ", chat.error] }) })), chat.interrupted && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\u26A1 \u64CD\u4F5C\u88AB\u7528\u6237\u4E2D\u65AD\uFF0C\u7B49\u5F85\u65B0\u7684\u4EFB\u52A1\u8F93\u5165..." }) })), _jsx(InputBox, { onSubmit: chat.submit, disabled: chat.isLoading, onAbort: chat.abort, onClear: handleClear, onCompact: chat.compactNow }), _jsx(Static, { items: [chat.history.length], children: () => (_jsx(StatusLine, { provider: provider, history: chat.history, pluginProgress: chat.pluginProgress })) })] }));
|
|
37
37
|
}
|
package/src/ui/MessageList.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
const MAX_TOOL_PREVIEW_LINES = 3;
|
|
4
|
-
export function MessageList({ history, streamingText }) {
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", children: [history.map((m, i) => (_jsx(MessageRow, { message: m }, i))),
|
|
4
|
+
export function MessageList({ history, streamingText, streamingReasoning }) {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", children: [history.map((m, i) => (_jsx(MessageRow, { message: m }, i))), streamingReasoning.length > 0 && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\uD83D\uDCA1 ", streamingReasoning.length > 300
|
|
6
|
+
? `...(${Math.round(streamingReasoning.length / 1000)}K字)${streamingReasoning.slice(-200)}`
|
|
7
|
+
: streamingReasoning] }) })), streamingText.length > 0 && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: streamingText }) }))] }));
|
|
6
8
|
}
|
|
7
9
|
function MessageRow({ message }) {
|
|
8
10
|
switch (message.role) {
|
package/src/ui/hooks/useChat.js
CHANGED
|
@@ -29,6 +29,7 @@ export function useChat(args) {
|
|
|
29
29
|
const [version, setVersion] = useState(0);
|
|
30
30
|
const bump = useCallback(() => setVersion((v) => v + 1), []);
|
|
31
31
|
const [streamingText, setStreamingText] = useState('');
|
|
32
|
+
const [streamingReasoning, setStreamingReasoning] = useState('');
|
|
32
33
|
const [toolStatus, setToolStatus] = useState(null);
|
|
33
34
|
const [isLoading, setIsLoading] = useState(false);
|
|
34
35
|
const [error, setError] = useState(null);
|
|
@@ -55,6 +56,49 @@ export function useChat(args) {
|
|
|
55
56
|
setPluginProgress(null);
|
|
56
57
|
const ac = new AbortController();
|
|
57
58
|
abortRef.current = ac;
|
|
59
|
+
let streamBuf = '';
|
|
60
|
+
let reasoningBuf = '';
|
|
61
|
+
let streamTimer = null;
|
|
62
|
+
const throttledSetStreamingText = (value) => {
|
|
63
|
+
streamBuf = typeof value === 'function' ? value(streamBuf) : value;
|
|
64
|
+
if (!streamTimer) {
|
|
65
|
+
streamTimer = setInterval(() => {
|
|
66
|
+
if (streamBuf.length > 0) {
|
|
67
|
+
setStreamingText(streamBuf);
|
|
68
|
+
}
|
|
69
|
+
if (reasoningBuf.length > 0) {
|
|
70
|
+
setStreamingReasoning(reasoningBuf);
|
|
71
|
+
}
|
|
72
|
+
}, 200);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const throttledSetStreamingReasoning = (value) => {
|
|
76
|
+
reasoningBuf = typeof value === 'function' ? value(reasoningBuf) : value;
|
|
77
|
+
if (!streamTimer) {
|
|
78
|
+
streamTimer = setInterval(() => {
|
|
79
|
+
if (streamBuf.length > 0) {
|
|
80
|
+
setStreamingText(streamBuf);
|
|
81
|
+
}
|
|
82
|
+
if (reasoningBuf.length > 0) {
|
|
83
|
+
setStreamingReasoning(reasoningBuf);
|
|
84
|
+
}
|
|
85
|
+
}, 200);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const flushStream = () => {
|
|
89
|
+
if (streamTimer) {
|
|
90
|
+
clearInterval(streamTimer);
|
|
91
|
+
streamTimer = null;
|
|
92
|
+
}
|
|
93
|
+
if (streamBuf.length > 0) {
|
|
94
|
+
setStreamingText(streamBuf);
|
|
95
|
+
streamBuf = '';
|
|
96
|
+
}
|
|
97
|
+
if (reasoningBuf.length > 0) {
|
|
98
|
+
setStreamingReasoning(reasoningBuf);
|
|
99
|
+
reasoningBuf = '';
|
|
100
|
+
}
|
|
101
|
+
};
|
|
58
102
|
try {
|
|
59
103
|
for await (const ev of runWithPlugins(trimmed, {
|
|
60
104
|
provider: args.provider,
|
|
@@ -62,7 +106,8 @@ export function useChat(args) {
|
|
|
62
106
|
signal: ac.signal,
|
|
63
107
|
})) {
|
|
64
108
|
handleEvent(ev, {
|
|
65
|
-
setStreamingText,
|
|
109
|
+
setStreamingText: throttledSetStreamingText,
|
|
110
|
+
setStreamingReasoning: throttledSetStreamingReasoning,
|
|
66
111
|
setToolStatus,
|
|
67
112
|
setCompacting,
|
|
68
113
|
setError,
|
|
@@ -76,8 +121,10 @@ export function useChat(args) {
|
|
|
76
121
|
setError(`未捕获异常:${e.message}`);
|
|
77
122
|
}
|
|
78
123
|
finally {
|
|
124
|
+
flushStream();
|
|
79
125
|
setIsLoading(false);
|
|
80
126
|
setStreamingText('');
|
|
127
|
+
setStreamingReasoning('');
|
|
81
128
|
setToolStatus(null);
|
|
82
129
|
setCompacting(false);
|
|
83
130
|
setPluginProgress(null);
|
|
@@ -104,6 +151,7 @@ export function useChat(args) {
|
|
|
104
151
|
// 允许新 session 再次触发反应式压缩自救
|
|
105
152
|
resetReactiveCompactState();
|
|
106
153
|
setStreamingText('');
|
|
154
|
+
setStreamingReasoning('');
|
|
107
155
|
setToolStatus(null);
|
|
108
156
|
setError(null);
|
|
109
157
|
setInterrupted(false);
|
|
@@ -156,6 +204,7 @@ export function useChat(args) {
|
|
|
156
204
|
return {
|
|
157
205
|
history,
|
|
158
206
|
streamingText,
|
|
207
|
+
streamingReasoning,
|
|
159
208
|
toolStatus,
|
|
160
209
|
isLoading,
|
|
161
210
|
error,
|
|
@@ -179,11 +228,16 @@ function handleEvent(ev, setters) {
|
|
|
179
228
|
const e = ev;
|
|
180
229
|
switch (e.type) {
|
|
181
230
|
case 'text':
|
|
182
|
-
setters.setStreamingText(
|
|
231
|
+
setters.setStreamingText(prev => prev + e.delta);
|
|
232
|
+
break;
|
|
233
|
+
case 'reasoning_delta':
|
|
234
|
+
if (e.delta) {
|
|
235
|
+
setters.setStreamingReasoning(prev => prev + e.delta);
|
|
236
|
+
}
|
|
183
237
|
break;
|
|
184
238
|
case 'assistant_message':
|
|
185
|
-
|
|
186
|
-
setters.
|
|
239
|
+
setters.setStreamingText('');
|
|
240
|
+
setters.setStreamingReasoning('');
|
|
187
241
|
setters.bump();
|
|
188
242
|
break;
|
|
189
243
|
case 'tool_start':
|
|
@@ -199,10 +253,8 @@ function handleEvent(ev, setters) {
|
|
|
199
253
|
setters.bump();
|
|
200
254
|
break;
|
|
201
255
|
case 'compact_start':
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// autoCompact 场景下 streamingText 本来就是空,这里 no-op。
|
|
205
|
-
setters.setStreamingText(() => '');
|
|
256
|
+
setters.setStreamingText('');
|
|
257
|
+
setters.setStreamingReasoning('');
|
|
206
258
|
setters.setCompacting(true);
|
|
207
259
|
setters.bump();
|
|
208
260
|
break;
|
|
@@ -215,10 +267,14 @@ function handleEvent(ev, setters) {
|
|
|
215
267
|
break;
|
|
216
268
|
case 'interrupted':
|
|
217
269
|
setters.setInterrupted(true);
|
|
270
|
+
setters.setStreamingText('');
|
|
271
|
+
setters.setStreamingReasoning('');
|
|
218
272
|
setters.bump();
|
|
219
273
|
break;
|
|
220
274
|
case 'error':
|
|
221
275
|
setters.setError(e.error);
|
|
276
|
+
setters.setStreamingText('');
|
|
277
|
+
setters.setStreamingReasoning('');
|
|
222
278
|
setters.bump();
|
|
223
279
|
break;
|
|
224
280
|
case 'plugin_progress':
|