agent-from-zero 0.35.4 → 0.35.5
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/README.md +6 -0
- package/dist/config.js +4 -0
- package/dist/ink/app.js +25 -1
- package/dist/ink/commands.js +1 -0
- package/dist/loop.js +11 -1
- package/dist/suggestion.js +55 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -115,6 +115,12 @@ export MINI_AGENT_MODEL=gpt-4.1-mini
|
|
|
115
115
|
|
|
116
116
|
输入 `/mcp` 选一个服务器,会看到和 Claude Code 一样的详情(状态、登录、协议版本、URL、配置文件位置、能力、工具数)和操作:View tools、Re-authenticate、Clear authentication、Reconnect、Disable。也可以直接敲 `/mcp tools|auth|clear-auth|reconnect|disable|enable <名字>`。
|
|
117
117
|
|
|
118
|
+
### 下一句预测
|
|
119
|
+
|
|
120
|
+
和 Claude Code 一样,每轮对话结束后,mini-agent 会猜你下一句大概要说什么(比如 “跑一下测试”、“提交并推送”),灰色显示在空的输入框里。按 Tab(或 →)变成真正的输入,可以改,也可以直接 Enter 发送。开始打字它就消失。
|
|
121
|
+
|
|
122
|
+
生成预测要多调一次模型,但请求内容和刚结束的主调用相同,只在最后加了一条指令,所以基本都能命中 DeepSeek 的前缀缓存,费用很低,也会算进 `/cost`。定时任务和其他实例发来的消息触发的回合不做预测。不想要可以在 `settings.json` 里设 `"promptSuggestions": false`,或者单次运行时设环境变量 `MINI_AGENT_PROMPT_SUGGESTIONS=0`。
|
|
123
|
+
|
|
118
124
|
### 多开实例互相通信
|
|
119
125
|
|
|
120
126
|
在几个终端里各开一个 mini-agent(比如一个在后端仓库、一个在前端仓库),它们会自动互相发现,不用配置。
|
package/dist/config.js
CHANGED
|
@@ -136,6 +136,10 @@ export const CONFIG = {
|
|
|
136
136
|
memory: {
|
|
137
137
|
autoExtract: projectSettings.memory?.autoExtract ?? globalSettings.memory?.autoExtract ?? false,
|
|
138
138
|
},
|
|
139
|
+
// Next-prompt suggestions (Claude Code's placeholder you accept with Tab). On
|
|
140
|
+
// by default: the call reuses the main conversation's cached prefix, so it is
|
|
141
|
+
// cheap. MINI_AGENT_PROMPT_SUGGESTIONS=0 turns it off for one run.
|
|
142
|
+
promptSuggestions: process.env.MINI_AGENT_PROMPT_SUGGESTIONS === "0" ? false : (projectSettings.promptSuggestions ?? globalSettings.promptSuggestions ?? true),
|
|
139
143
|
};
|
|
140
144
|
// Persist a single key to the global settings file, preserving all other keys.
|
|
141
145
|
// Used by /model to remember the user's last choice across sessions.
|
package/dist/ink/app.js
CHANGED
|
@@ -10,6 +10,8 @@ import { initFormState, reduceForm, renderForm, collectAnswers } from "../form.j
|
|
|
10
10
|
import { CONFIG, saveGlobalSetting } from "../config.js"; // session allowlist + /model save
|
|
11
11
|
import { effortMenu, setEffort } from "../effort.js";
|
|
12
12
|
import { useSlashCompletion } from "./completion.js";
|
|
13
|
+
import { generatePromptSuggestion } from "../suggestion.js"; // the predicted next message (Claude Code's prompt suggestion)
|
|
14
|
+
import { lastRequestTools } from "../loop.js"; // resend the main call's tools so the guess hits the prompt cache
|
|
13
15
|
import { commandNames, commandTokenRanges, inlineCommandGhost } from "../completion.js"; // which /command tokens to paint + the mid-text ghost suggestion
|
|
14
16
|
import { TerminateReason, listAgentViews } from "../loop.js"; // how a turn can end
|
|
15
17
|
import { compactHistory, compactThreshold, contextWindowFor } from "../context.js"; // /compact + /model info
|
|
@@ -178,6 +180,8 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
178
180
|
const [detailOffset, setDetailOffset] = useState(0);
|
|
179
181
|
const [live, setLive] = useState(null); // the streaming answer
|
|
180
182
|
const [input, setInput] = useState(""); // the current input buffer
|
|
183
|
+
const [suggestion, setSuggestion] = useState(null); // the predicted next message: a dim placeholder, Tab accepts it
|
|
184
|
+
const suggestionSeq = useRef(0); // bumped by every new turn, so a late guess for an old turn is dropped
|
|
181
185
|
const [talkTo, setTalkTo] = useState(null); // /peers → talk: the input box sends to another session instead of this agent
|
|
182
186
|
const replyTo = useRef([]); // who gets this turn's final answer (the senders of "user" peer messages)
|
|
183
187
|
const [images, setImages] = useState([]);
|
|
@@ -225,6 +229,8 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
225
229
|
}, rows - 10, columns);
|
|
226
230
|
// A "/" later in the text: no list, just the rest of the best match as dim ghost text (Tab accepts).
|
|
227
231
|
const ghost = completing && cursor === input.length ? inlineCommandGhost(input, { skills: typeaheadSkills }) : null;
|
|
232
|
+
// The predicted next message, shown only in an empty box while nothing else wants attention.
|
|
233
|
+
const placeholder = suggestion && !input && !busy && !pending && !talkTo && !details && subAgentFocus === null ? suggestion : null;
|
|
228
234
|
useEffect(() => {
|
|
229
235
|
const id = setInterval(() => setTick((t) => t + 1), 1000);
|
|
230
236
|
return () => clearInterval(id);
|
|
@@ -253,6 +259,11 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
253
259
|
clearToolCalls(); // ...and Ctrl+T THIS turn's tool calls
|
|
254
260
|
const senders = replyTo.current; // taken now, so a failed turn can't leak them into the next one
|
|
255
261
|
replyTo.current = [];
|
|
262
|
+
// Guess the next message only after turns YOU started — not cron, not
|
|
263
|
+
// another agent's message, not your message relayed from another window.
|
|
264
|
+
const suggestAfter = CONFIG.promptSuggestions && !senders.length && (content === null || display?.kind === "user");
|
|
265
|
+
const seq = ++suggestionSeq.current;
|
|
266
|
+
setSuggestion(null);
|
|
256
267
|
setBusy(true);
|
|
257
268
|
setSessionState("busy", CONFIG.model); // other sessions see "busy" in list_peers
|
|
258
269
|
const controller = new AbortController();
|
|
@@ -294,6 +305,12 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
294
305
|
if (result.reason !== TerminateReason.Done && !(result.reason === TerminateReason.UserInterrupt && followUps.size))
|
|
295
306
|
note(chalk.yellow(`⚠️ ${EXIT_NOTES[result.reason] ?? result.reason}`));
|
|
296
307
|
saveSession(sessionId, model, messages, pendingTitle.current); // snapshot after every turn — crash-safe by construction
|
|
308
|
+
if (suggestAfter && result.reason === TerminateReason.Done) {
|
|
309
|
+
void generatePromptSuggestion(client, CONFIG.model, messages, lastRequestTools()).then((guess) => {
|
|
310
|
+
if (guess && seq === suggestionSeq.current)
|
|
311
|
+
setSuggestion(guess); // no newer turn started meanwhile (the box shows it only while idle)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
297
314
|
if (CONFIG.memory.autoExtract && result.reason === TerminateReason.Done) {
|
|
298
315
|
try {
|
|
299
316
|
const got = await extractMemories(client, CONFIG.subAgentModel || model, messages);
|
|
@@ -952,6 +969,13 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
952
969
|
return;
|
|
953
970
|
}
|
|
954
971
|
}
|
|
972
|
+
if ((key.tab || (key.rightArrow && !input)) && placeholder) { // Tab (or →) turns the predicted next message into real text; Enter then sends it
|
|
973
|
+
setInput(placeholder);
|
|
974
|
+
setCursor(placeholder.length);
|
|
975
|
+
setHistIdx(null);
|
|
976
|
+
setSuggestion(null);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
955
979
|
if (key.tab && ghost) { // accept the inline suggestion as "/name "
|
|
956
980
|
const next = input.slice(0, ghost.start) + `/${ghost.full} `;
|
|
957
981
|
setInput(next);
|
|
@@ -1193,7 +1217,7 @@ export function App({ session, runTurn, clipboard }) {
|
|
|
1193
1217
|
const at = line.slice(cuOff, cuOff + 1) || " ";
|
|
1194
1218
|
const after = line.slice(cuOff + 1);
|
|
1195
1219
|
const base = lineStarts[i] ?? 0;
|
|
1196
|
-
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(before, base, "b") }), _jsx(Text, { inverse: true, color: isLit(base + cuOff) ? SKILL_COLOR : undefined, children: ghost ? ghost.suffix[0] : at }), _jsx(Text, { children: paint(after, base + cuOff + at.length, "a") }), ghost && _jsx(Text, { dimColor: true, children: ghost.suffix.slice(1) })] }, i));
|
|
1220
|
+
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(before, base, "b") }), _jsx(Text, { inverse: true, color: isLit(base + cuOff) ? SKILL_COLOR : undefined, children: ghost ? ghost.suffix[0] : placeholder && i === 0 ? [...placeholder][0] : at }), _jsx(Text, { children: paint(after, base + cuOff + at.length, "a") }), ghost && _jsx(Text, { dimColor: true, children: ghost.suffix.slice(1) }), placeholder && i === 0 && _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [[...placeholder].slice(1).join(""), _jsx(Text, { dimColor: true, children: " ⇥ tab" })] })] }, i));
|
|
1197
1221
|
}
|
|
1198
1222
|
return (_jsxs(Box, { children: [i === 0 && _jsx(Text, { color: promptColor, children: prompt }), _jsx(Text, { children: paint(line, lineStarts[i] ?? 0, "l") })] }, i));
|
|
1199
1223
|
});
|
package/dist/ink/commands.js
CHANGED
|
@@ -46,6 +46,7 @@ export const SESSION_HELP = `commands:
|
|
|
46
46
|
exit leave (Ctrl+C does the same)
|
|
47
47
|
|
|
48
48
|
keys (during or after a turn):
|
|
49
|
+
Tab / → accept the predicted next message (the dim placeholder after a turn); Enter sends it
|
|
49
50
|
/ show command suggestions below the input (while idle)
|
|
50
51
|
↑↓ / Tab choose / complete a suggestion; Enter selects; Esc closes
|
|
51
52
|
Enter while working, queue a follow-up for the next tool boundary
|
package/dist/loop.js
CHANGED
|
@@ -44,6 +44,16 @@ export var TerminateReason;
|
|
|
44
44
|
TerminateReason["FatalApiError"] = "fatal_api_error";
|
|
45
45
|
TerminateReason["UserInterrupt"] = "user_interrupt";
|
|
46
46
|
})(TerminateReason || (TerminateReason = {}));
|
|
47
|
+
// The tool list of the last TOP-LEVEL model call. The next-prompt suggestion
|
|
48
|
+
// (suggestion.ts) resends the same tools so its request shares the main
|
|
49
|
+
// conversation's prefix — and the provider's prompt cache with it.
|
|
50
|
+
let lastTopLevelTools = [];
|
|
51
|
+
export const lastRequestTools = () => lastTopLevelTools;
|
|
52
|
+
function rememberTools(opts, tools) {
|
|
53
|
+
if (!opts.subAgent)
|
|
54
|
+
lastTopLevelTools = tools;
|
|
55
|
+
return tools;
|
|
56
|
+
}
|
|
47
57
|
// Resolve the output sink for a loop: the caller's sink, or stdout (the old
|
|
48
58
|
// behaviour) when none was passed. Centralised so every site reads the same way.
|
|
49
59
|
function sink(opts) {
|
|
@@ -456,7 +466,7 @@ opts) {
|
|
|
456
466
|
// The tool manual depends on the agent kind (Lead / teammate / sub-agent)
|
|
457
467
|
// — see toolsFor(). This is also where nested spawning is prevented: a
|
|
458
468
|
// teammate's manual simply omits spawn_teammate and task.
|
|
459
|
-
{ model: opts.model, messages: effortMessages(opts.model, messages), ...effortParameters(opts.model), tools: toolsFor(opts), stream: true, stream_options: { include_usage: true } }, { signal });
|
|
469
|
+
{ model: opts.model, messages: effortMessages(opts.model, messages), ...effortParameters(opts.model), tools: rememberTools(opts, toolsFor(opts)), stream: true, stream_options: { include_usage: true } }, { signal });
|
|
460
470
|
let content = ""; // accumulated answer text
|
|
461
471
|
const calls = []; // tool calls under assembly, indexed by delta.index
|
|
462
472
|
// Stash thinking without appending one indicator per model round. The live
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { effortMessages, effortProfile } from "./effort.js"; // same message shape as the main loop sends
|
|
2
|
+
import { recordUsage } from "./cost.js"; // the suggestion call shows up in /cost like any other
|
|
3
|
+
// ---- Next-prompt suggestion (Claude Code's prompt suggestion) -------------------
|
|
4
|
+
// When a turn ends, guess what you'll type next and show it as a dim placeholder
|
|
5
|
+
// in the empty input box: Tab turns it into real text, Enter sends it. It's
|
|
6
|
+
// most useful at the obvious moments — "run the tests", "commit it", "yes, do
|
|
7
|
+
// the same for the other file" — so the guess must be short and concrete, or
|
|
8
|
+
// nothing at all.
|
|
9
|
+
//
|
|
10
|
+
// Cost: the request is the SAME history plus the SAME tools as the main call
|
|
11
|
+
// that just finished, with one instruction appended. That shared prefix is
|
|
12
|
+
// what the provider's prompt cache keys on (DeepSeek caches automatically),
|
|
13
|
+
// so almost all of the input is billed at the cached rate. tool_choice "none"
|
|
14
|
+
// keeps the tools in the prompt (cache) without letting the model call one.
|
|
15
|
+
const SUGGESTION_TIMEOUT_MS = 15_000;
|
|
16
|
+
const MAX_CHARS = 120;
|
|
17
|
+
const SUGGESTION_PROMPT = `[Next-prompt suggestion — this message is from the app, not the user]
|
|
18
|
+
Predict the user's NEXT message in this conversation: what they would most likely type now, given what just happened.
|
|
19
|
+
- Write it AS THE USER, addressed to the assistant (e.g. "run the tests", "commit and push", "也改一下另一个文件").
|
|
20
|
+
- Short and concrete: at most 12 words, or 30 characters for Chinese/Japanese. Same language the user writes in.
|
|
21
|
+
- Only a natural next step the user would actually want. No questions about preferences, no thanks, no pleasantries.
|
|
22
|
+
- If there is no clear next step, reply exactly: NONE
|
|
23
|
+
Reply with ONLY the message text — no quotes, no explanation.`;
|
|
24
|
+
// Keep only something that reads like a user's short next message. Anything
|
|
25
|
+
// else — an answer, a plan, the assistant's own voice — is worse than nothing.
|
|
26
|
+
export function cleanSuggestion(raw) {
|
|
27
|
+
let s = raw.trim().replace(/^["'“”「」`]+|["'“”「」`]+$/g, "").trim();
|
|
28
|
+
if (!s || /^none\.?$/i.test(s) || s.includes("\n"))
|
|
29
|
+
return null;
|
|
30
|
+
if (s.length > MAX_CHARS)
|
|
31
|
+
return null;
|
|
32
|
+
if (/^(i'll|i will|i've|i have|let me|sure|okay,? i|here's|the user)\b/i.test(s))
|
|
33
|
+
return null; // the assistant talking, not the user
|
|
34
|
+
if (/^(好的|我来|我会|我已经|用户)/.test(s))
|
|
35
|
+
return null;
|
|
36
|
+
s = s.replace(/\s+/g, " ");
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
export async function generatePromptSuggestion(client, model, messages, tools = [], signal) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await client.chat.completions.create({
|
|
42
|
+
model, // the conversation's model: its cache holds this prefix
|
|
43
|
+
messages: [...effortMessages(model, messages), { role: "user", content: SUGGESTION_PROMPT }],
|
|
44
|
+
...(tools.length ? { tools, tool_choice: "none" } : {}),
|
|
45
|
+
...(effortProfile(model)?.parameter === "deepseek" ? { thinking: { type: "disabled" } } : {}), // a one-line guess needs no reasoning
|
|
46
|
+
max_tokens: 60,
|
|
47
|
+
stream: false,
|
|
48
|
+
}, { timeout: SUGGESTION_TIMEOUT_MS, signal });
|
|
49
|
+
recordUsage(res.usage);
|
|
50
|
+
return cleanSuggestion(res.choices?.[0]?.message?.content ?? "");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null; // a failed guess is simply no guess
|
|
54
|
+
}
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mini-agent": "dist/agent.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"auto:report": "tsx eval/auto-report.ts",
|
|
18
18
|
"build": "tsc -p tsconfig.build.json",
|
|
19
19
|
"prepare": "npm run build",
|
|
20
|
-
"test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/peers.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
20
|
+
"test": "export MINI_AGENT_REVIEW_DEBUG=0 && tsx tests/tools.test.ts && tsx tests/permissions.test.ts && tsx tests/context.test.ts && tsx tests/hooks.test.ts && tsx tests/mcp.test.ts && tsx tests/judge.test.ts && tsx tests/auto.test.ts && tsx tests/auto-context.test.ts && tsx tests/auto-providers.test.ts && tsx tests/auto-cascade.test.ts && tsx tests/auto-rules.test.ts && tsx tests/auto-rule-cascade.test.ts && tsx tests/auto-debug.test.ts && tsx tests/auto-report.test.ts && tsx tests/auto-clarification.test.ts && tsx tests/memory.test.ts && tsx tests/cost.test.ts && tsx tests/cli.test.ts && tsx tests/print.test.ts && tsx tests/status.test.ts && tsx tests/validate.test.ts && tsx tests/plan.test.ts && tsx tests/diff.test.ts && tsx tests/undo.test.ts && tsx tests/tiering.test.ts && tsx tests/mcp-http.test.ts && tsx tests/mcp-oauth.test.ts && tsx tests/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/title.test.ts && tsx tests/ui.test.ts && tsx tests/activity.test.ts && tsx tests/follow-up.test.ts && tsx tests/follow-up-ui.test.tsx && tsx tests/agents.test.ts && tsx tests/images.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/effort.test.ts && tsx tests/completion.test.ts && tsx tests/pasted-text.test.ts && tsx tests/skills.test.ts && tsx tests/markdown.test.ts && tsx tests/editor.test.ts && tsx tests/todos.test.ts && tsx tests/background.test.ts && tsx tests/team.test.ts && tsx tests/peers.test.ts && tsx tests/suggestion.test.ts && tsx tests/board.test.ts && tsx tests/drop.test.ts && tsx tests/editline.test.ts && tsx tests/cron.test.ts"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20.6"
|