agent-from-zero 0.34.2 → 0.35.1

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/dist/ink/chat.js CHANGED
@@ -1,12 +1,18 @@
1
+ import { AutoReviewBudget } from "../auto-budget.js";
1
2
  import { runLoop } from "../loop.js"; // the REAL agent loop — tools, permissions, retries, the lot
2
3
  import { CONFIG } from "../config.js"; // model + sub-agent tier
4
+ import { userContent } from "../images.js";
3
5
  // Drive one real turn through the loop. The loop mutates `messages` in place
4
6
  // (appends the assistant reply + any tool messages), exactly as the readline
5
7
  // REPL relies on — so we only push the user turn here and let the loop do the
6
8
  // rest. All screen output flows through hooks.output (the Ink sink).
7
9
  export function makeRunTurn(client, messages) {
10
+ let reviewBudget = new AutoReviewBudget();
8
11
  return async (input, hooks) => {
9
- messages.push({ role: "user", content: input });
12
+ if (input !== null)
13
+ reviewBudget = new AutoReviewBudget();
14
+ if (input !== null)
15
+ messages.push({ role: "user", content: userContent(input, hooks.images ?? []) });
10
16
  return runLoop(messages, {
11
17
  client,
12
18
  model: CONFIG.model,
@@ -15,6 +21,11 @@ export function makeRunTurn(client, messages) {
15
21
  confirm: hooks.confirm,
16
22
  askUser: hooks.askUser,
17
23
  judge: hooks.judge,
24
+ autoMode: hooks.autoMode,
25
+ reviewBudget,
26
+ followUps: hooks.followUps,
27
+ onFollowUp: hooks.onFollowUp,
28
+ canPrompt: !!process.stdin.isTTY,
18
29
  subAgentModel: CONFIG.subAgentModel,
19
30
  output: hooks.output,
20
31
  });
@@ -23,9 +23,13 @@ export const SESSION_HELP = `commands:
23
23
  /model switch model for THIS session: "/model" to pick from a list, "/model <name>" to set; "/model save <name>" to make it your default
24
24
  /stats event counts for this session (local telemetry — nothing leaves this machine)
25
25
  /memory show the durable facts the agent remembers about this project
26
+ /status session usage and vendor account balance/cost
26
27
  /cost tokens, cache hit rate and estimated spend this session (local)
27
28
  /mcp list configured MCP servers + status; select one to authenticate / reconnect / disable
28
29
  /plan toggle plan mode — research-only; the agent presents a plan you approve before any change
30
+ /effort [level] list/select the current model’s supported reasoning effort
31
+ /auto toggle auto mode — risky or uncertain actions still require approval
32
+ /auto debug [on|off|status] inspect permission reviewer requests and replies
29
33
  /todos show the agent's current task plan (it maintains one with todo_write on multi-step work)
30
34
  /bg list background tasks this session (run_bash_background) and their status
31
35
  /team list the agent team (spawn_teammate): each teammate's role, status, and pending inbox
@@ -38,9 +42,18 @@ export const SESSION_HELP = `commands:
38
42
  /skill <name> run a skill yourself (works even for user-only skills)
39
43
  exit leave (Ctrl+C does the same)
40
44
 
41
- keys (at the prompt):
42
- Ctrl+R reveal the model's thinking for the last answer (collapsed behind a spinner by default)
43
- Ctrl+T reveal the folded tool-call trace for the last answer`;
45
+ keys (during or after a turn):
46
+ / show command suggestions below the input (while idle)
47
+ ↑↓ / Tab choose / complete a suggestion; Enter selects; Esc closes
48
+ Enter while working, queue a follow-up for the next tool boundary
49
+ Ctrl+Enter interrupt and send now (Esc also sends already-queued messages)
50
+ Ctrl+V paste an image as [Image #1], or paste text
51
+ Tab / ← focus the agent list (← at the input boundary)
52
+ ↑ / ↓ select an agent; Enter views it; Esc returns to main
53
+ Ctrl+R toggle thinking details
54
+ Ctrl+T toggle tool arguments and results
55
+ ↑ / ↓ page through open details (PgUp/PgDn also work)
56
+ Esc close details first; otherwise interrupt and process queued input`;
44
57
  // Colored status word for an MCP server (shared by the Ink + readline /mcp UIs).
45
58
  export function mcpStatusText(info) {
46
59
  switch (info.status) {
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { slashCompletions } from "../completion.js";
5
+ // Keep the full list in state, but render only a window around the selection.
6
+ // Long lists must never grow Ink's redraw region beyond the terminal height.
7
+ export function useSlashCompletion(input, enabled, context, maxRows) {
8
+ const [selection, setSelection] = useState({ input: "", index: 0 });
9
+ const [dismissed, setDismissed] = useState(null);
10
+ useEffect(() => { setDismissed(null); }, [input]);
11
+ const menu = enabled && dismissed !== input ? slashCompletions(input, context) : null;
12
+ const items = menu?.items ?? [];
13
+ const selected = Math.min(selection.input === input ? selection.index : 0, Math.max(0, items.length - 1));
14
+ const count = Math.max(1, Math.min(8, maxRows));
15
+ const start = Math.max(0, Math.min(selected - Math.floor(count / 2), items.length - count));
16
+ return {
17
+ visible: menu !== null,
18
+ handleKey: (key, replace) => {
19
+ if (!menu)
20
+ return { handled: false };
21
+ if (key.escape) {
22
+ setDismissed(input);
23
+ return { handled: true };
24
+ }
25
+ if (!items.length)
26
+ return { handled: false };
27
+ if (key.upArrow || key.downArrow) {
28
+ setSelection({ input, index: (selected + (key.upArrow ? -1 : 1) + items.length) % items.length });
29
+ return { handled: true };
30
+ }
31
+ if (key.tab || (key.return && !key.ctrl && !key.meta)) {
32
+ const item = items[selected];
33
+ if (key.tab || item.children)
34
+ replace(item.value + (item.children ? " " : ""));
35
+ else
36
+ return { handled: true, submit: item.value };
37
+ return { handled: true };
38
+ }
39
+ return { handled: false };
40
+ },
41
+ view: menu && (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [items.slice(start, start + count).map((item, i) => (_jsxs(Text, { color: start + i === selected ? "cyan" : undefined, bold: start + i === selected, wrap: "truncate-end", children: [start + i === selected ? "› " : " ", item.value, " ", _jsx(Text, { dimColor: true, children: item.description.replace(/\s+/g, " ") })] }, item.value))), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: items.length ? `↑↓ choose · Tab complete · Enter select · Esc close · ${selected + 1}/${items.length}` : menu.hint || "No matching commands" })] })),
42
+ };
43
+ }
package/dist/ink/setup.js CHANGED
@@ -2,7 +2,7 @@ import OpenAI from "openai";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  import { CONFIG, requireApiKey } from "../config.js";
5
- import { buildSystemMessage } from "../prompt.js";
5
+ import { buildSystemMessage, readProjectInstructions } from "../prompt.js";
6
6
  import { estimateHistoryTokens } from "../context.js";
7
7
  import { initCostMeter, DEFAULT_PRICING } from "../cost.js";
8
8
  import { gitBranch } from "../tui.js";
@@ -11,6 +11,7 @@ import { initTelemetry, emit } from "../telemetry.js";
11
11
  import { runHooks } from "../hooks.js";
12
12
  import { connectMcpServers } from "../mcp.js";
13
13
  import { Judge } from "../judge.js";
14
+ import { AutoMode } from "../auto.js";
14
15
  import { registerExternalTool } from "../tools.js";
15
16
  import { rememberTool, readMemory } from "../memory.js";
16
17
  import { loadSkills, buildSkillTool } from "../skills.js";
@@ -30,7 +31,8 @@ const pkg = createRequire(import.meta.url)("../../package.json");
30
31
  export async function buildInkSession(opts = {}) {
31
32
  requireApiKey();
32
33
  const client = new OpenAI({ baseURL: CONFIG.baseURL, apiKey: CONFIG.apiKey, maxRetries: 0 });
33
- const systemMessage = buildSystemMessage(); // stable prefix = cache hits every request
34
+ const projectInstructions = readProjectInstructions();
35
+ const systemMessage = buildSystemMessage(projectInstructions); // stable prefix = cache hits every request
34
36
  const messages = [{ role: "system", content: systemMessage }];
35
37
  const notices = [];
36
38
  let sessionId = newSessionId();
@@ -63,9 +65,11 @@ export async function buildInkSession(opts = {}) {
63
65
  if (cronCount)
64
66
  notices.push(`(cron: ${cronCount} durable job${cronCount !== 1 ? "s" : ""} loaded)`);
65
67
  startCronScheduler();
66
- const judge = CONFIG.judge.enabled ? new Judge(client, CONFIG.judge.model || CONFIG.model) : undefined;
68
+ const judge = CONFIG.judge.enabled && !CONFIG.bypassPermissions ? new Judge(client, CONFIG.judge.model || CONFIG.model) : undefined;
69
+ const autoMode = new AutoMode(client, { projectInstructions });
70
+ notices.push(...autoMode.startupNotices());
67
71
  if (judge)
68
- notices.push(`(permission judge on — model ${CONFIG.judge.model || CONFIG.model})`);
72
+ notices.push(`(permission judge on — ${autoMode.backend})`);
69
73
  // The remember tool flows through the same permission gate as any tool.
70
74
  registerExternalTool(rememberTool);
71
75
  const memCount = readMemory().length;
@@ -104,6 +108,7 @@ export async function buildInkSession(opts = {}) {
104
108
  costMeter,
105
109
  skills,
106
110
  judge,
111
+ autoMode,
107
112
  model: CONFIG.model,
108
113
  dir,
109
114
  branch,
package/dist/ink/sink.js CHANGED
@@ -1,22 +1,32 @@
1
1
  // Build a LoopOutput backed by Ink state. Pure glue: every method maps a loop
2
2
  // event to a state change.
3
3
  export function makeInkSink(api) {
4
+ // Concurrent reads can finish out of order. Removing one handle must not
5
+ // clear another call's animation, and stopped handles cannot revive it.
6
+ const statuses = new Map();
7
+ const refresh = () => api.setStatus([...statuses.values()].at(-1) ?? null);
4
8
  return {
9
+ streamingStatus: true,
5
10
  // A spinner becomes the single "status" line below the conversation. A fresh
6
11
  // handle per model call (the loop makes one per round); `active` mirrors
7
12
  // ora's isSpinning so the loop's `spinner?.spinning` checks behave the same.
8
13
  spinner(text) {
9
- api.setStatus(text);
14
+ const id = Symbol();
15
+ statuses.set(id, text);
16
+ refresh();
10
17
  let active = true;
11
18
  return {
12
19
  set: (t) => {
13
- if (active)
14
- api.setStatus(t);
20
+ if (active) {
21
+ statuses.set(id, t);
22
+ refresh();
23
+ }
15
24
  },
16
25
  stop: () => {
17
26
  if (active) {
18
27
  active = false;
19
- api.setStatus(null);
28
+ statuses.delete(id);
29
+ refresh();
20
30
  }
21
31
  },
22
32
  get spinning() {
@@ -24,8 +34,8 @@ export function makeInkSink(api) {
24
34
  },
25
35
  };
26
36
  },
27
- // The "💭 thought for Ns" indicator is just a committed line.
28
- reasoning: (line) => api.pushItem({ kind: "note", text: line }),
37
+ // The trace is already stored for Ctrl+R; no per-round scrollback noise.
38
+ reasoning: () => { },
29
39
  // The streamed answer accumulates in the dynamic region as raw text (live,
30
40
  // "it's typing"), then commits as ONE markdown-rendered item when it ends —
31
41
  // partial markdown mid-stream renders badly, so we format only once it's whole.
@@ -0,0 +1,15 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useStdout } from "ink";
3
+ // Size the dynamic viewport against the SAME output stream Ink renders to.
4
+ // Resize must trigger React too: Ink's Yoga relayout alone cannot repaginate text.
5
+ export function useTerminalSize() {
6
+ const { stdout } = useStdout();
7
+ const read = () => ({ rows: stdout.rows || 24, columns: stdout.columns || 80 });
8
+ const [size, setSize] = useState(read);
9
+ useEffect(() => {
10
+ const resize = () => setSize(read());
11
+ stdout.on("resize", resize);
12
+ return () => { stdout.off("resize", resize); };
13
+ }, [stdout]);
14
+ return size;
15
+ }