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 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
  });
@@ -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.4",
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"