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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.5.3",
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
  }
@@ -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
  }
@@ -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))), streamingText.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: streamingText }), _jsx(Text, { color: "gray", children: " \u258D" })] }))] }));
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) {
@@ -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((prev) => prev + e.delta);
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
- // history 已被 runQuery 直接 push,这里只刷一下视图 + 清流式 buffer
186
- setters.setStreamingText(() => '');
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
- // reactive 自救场景:LLM 失败前可能已经 yield 过部分 text delta,
203
- // 压缩前清掉残留 streamingText,避免重发后新文本追加在旧 buffer 后面。
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':