agent-from-zero 0.27.0 → 0.28.0

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/tools.js CHANGED
@@ -5,6 +5,8 @@ import { spawn } from "node:child_process"; // async process execution (does NOT
5
5
  import { validateArgs } from "./validate.js"; // schema validation for tool arguments
6
6
  import { setPlanMode } from "./permissions.js"; // plan mode (Day 20): exit_plan_mode flips it off on approval
7
7
  import { recordMutation } from "./undo.js"; // capture the before-state so /undo (Day 22) can restore it
8
+ import { parseTodos, setTodos, summarizeTodos } from "./todos.js"; // the agent's own checklist (Day 36)
9
+ import { startBackground, readBackground } from "./background.js"; // long-running commands that outlive the turn (Day 37)
8
10
  // ============ Session state ============
9
11
  // The foundation of "read before edit": which files has this session actually read?
10
12
  // Files that were never read must not be edited — otherwise the model edits from
@@ -235,6 +237,45 @@ On error: failures return the error output — analyze it and try a different ap
235
237
  });
236
238
  }),
237
239
  };
240
+ // ============ run_bash_background (Day 37) ============
241
+ // run_bash blocks and dies at its 30s watchdog — useless for anything truly
242
+ // slow. This is its asynchronous twin: it spawns the command and returns a task
243
+ // id IMMEDIATELY, so the agent keeps reasoning while the work happens. The loop
244
+ // injects a <task_notification> when the command finishes (background.ts). Use
245
+ // it for installs, builds, full test runs, or servers that never exit — exactly
246
+ // the keywords that signal a slow operation. The command goes through the SAME
247
+ // permission gate as run_bash (see permissions.ts), so the danger rules are
248
+ // identical; only the blocking behavior differs.
249
+ const runBashBackground = {
250
+ definition: def("run_bash_background", `Run a slow bash command in the BACKGROUND and return immediately with a task id (e.g. bg_1) — do NOT wait for it.
251
+ When to use: anything that would exceed run_bash's 30s limit or block you — installing dependencies (npm/pip install), builds (npm run build, cargo build, make), full test suites (pytest, npm test), deploys, or long-running servers (npm run dev) that never exit.
252
+ What you get back: just the task id. The command keeps running. When it finishes you'll receive a <task_notification> with its status and output tail — react to that. For a server or a job still running, poll it with bash_output.
253
+ Boundaries & permissions are identical to run_bash (dangerous commands are still gated or denied). For quick commands use run_bash instead — waiting on a one-off background job wastes a turn.`, { command: { type: "string", description: "The bash command to run in the background" } }, ["command"]),
254
+ // Fire-and-return: start the process, hand back the id. The output is captured
255
+ // by background.ts and delivered later as a notification — never inline here.
256
+ run: (args) => {
257
+ const id = startBackground(args.command);
258
+ return `Started background task ${id}: ${args.command}\nIt is running now; you will get a <task_notification> when it finishes. Keep working — use bash_output with task_id "${id}" to check on it (e.g. to confirm a server came up).`;
259
+ },
260
+ };
261
+ // ============ bash_output (Day 37) ============
262
+ // Poll a background task: its status and any output produced since the last
263
+ // poll. Essential for the case a notification can never cover — a server that
264
+ // runs forever (npm run dev) never "finishes", so the only way to see "ready on
265
+ // :3000" is to read its output. Reading advances a per-task cursor, so repeated
266
+ // polls show only what's new, not the whole log every time.
267
+ const bashOutput = {
268
+ definition: def("bash_output", `Check on a background task started with run_bash_background: returns its status and any NEW output since you last checked.
269
+ When to use: to see if a long job is done, or to read a long-running server's output (e.g. confirm "listening on :3000"). A finished task also sends you a <task_notification> automatically — you don't need to poll just to learn it ended.
270
+ Pass full=true to get the entire captured log instead of only the new lines. On an unknown task id you'll get the list of ids that exist.`, {
271
+ task_id: { type: "string", description: "The background task id, e.g. bg_1" },
272
+ full: { type: "boolean", description: "Return the entire captured output instead of only new output since the last check (default false)" },
273
+ }, ["task_id"]),
274
+ run: (args) => {
275
+ const full = args.full === true; // optional boolean
276
+ return spillIfHuge(readBackground(args.task_id, full), "background output");
277
+ },
278
+ };
238
279
  // ============ exit_plan_mode ============
239
280
  // Plan mode (Day 20) makes the agent research-only: the permission gate blocks
240
281
  // every mutating tool until the model presents a plan the user approves. The
@@ -251,6 +292,47 @@ On approval, plan mode turns off and writing/executing tools become available. I
251
292
  return "Plan approved by the user. Plan mode is OFF — implement the plan now; writing and executing tools are available again.";
252
293
  },
253
294
  };
295
+ // ============ todo_write ============
296
+ // The agent's planning tool (Day 36). The model sends the WHOLE list each time
297
+ // (it replaces the previous one); we store it, show it, and hand back a one-line
298
+ // tally. The array-of-objects schema is built by hand — the `def()` helper only
299
+ // does flat string params. State + rendering + the nag live in todos.ts.
300
+ const todoWrite = {
301
+ definition: {
302
+ type: "function",
303
+ function: {
304
+ name: "todo_write",
305
+ description: `Record and update a short plan for the CURRENT task as a checklist. This adds planning, NOT new abilities — it changes nothing on disk.
306
+ When to use: a task with 3+ distinct steps, or whenever the user gives several requirements. Skip it for trivial one-step asks.
307
+ How: pass the COMPLETE list every time (it replaces the previous one). Keep AT MOST ONE item in_progress. As you work, flip items to completed and move the next to in_progress — an out-of-date list is worse than none.`,
308
+ parameters: {
309
+ type: "object",
310
+ properties: {
311
+ todos: {
312
+ type: "array",
313
+ description: "The complete todo list, in order. Replaces the previous list.",
314
+ items: {
315
+ type: "object",
316
+ properties: {
317
+ content: { type: "string", description: "The step, phrased as an action" },
318
+ status: { type: "string", enum: ["pending", "in_progress", "completed"], description: "pending | in_progress | completed (only one in_progress)" },
319
+ },
320
+ required: ["content", "status"],
321
+ },
322
+ },
323
+ },
324
+ required: ["todos"],
325
+ },
326
+ },
327
+ },
328
+ run: (args) => {
329
+ const parsed = parseTodos(args.todos); // validate the model's list
330
+ if (parsed.error)
331
+ return fail(parsed.error); // precise repair message, not a crash
332
+ setTodos(parsed.todos); // store it (resets the nag clock); the loop renders it to the screen
333
+ return summarizeTodos(parsed.todos); // concise tally back to the model — the pretty list is for the human
334
+ },
335
+ };
254
336
  // ============ Registry & dispatch ============
255
337
  // The built-in tools. External tools (MCP servers, Day 15) are registered into
256
338
  // the same record at startup, so they flow through the same dispatch and the
@@ -262,7 +344,10 @@ export const tools = {
262
344
  edit_file: editFile,
263
345
  search,
264
346
  run_bash: runBash,
347
+ run_bash_background: runBashBackground,
348
+ bash_output: bashOutput,
265
349
  exit_plan_mode: exitPlanMode,
350
+ todo_write: todoWrite,
266
351
  };
267
352
  // Register an external tool (e.g. one discovered from an MCP server). Kept in
268
353
  // the same `tools` record so dispatch, the permission gate, and the tool-list
@@ -308,7 +393,7 @@ export async function dispatch(name, argsJson, signal) {
308
393
  // own output (preview + spill-to-disk via spillIfHuge); its cap must leave
309
394
  // room for the spill note, or dispatch would chop off the very pointer the
310
395
  // model needs to read the rest.
311
- const cap = name === "read_file" ? READ_LIMIT + 100 : name === "run_bash" ? BASH_OUTPUT_LIMIT + SPILL_NOTE_SLACK : TOOL_RESULT_LIMIT;
396
+ const cap = name === "read_file" ? READ_LIMIT + 100 : name === "run_bash" || name === "bash_output" ? BASH_OUTPUT_LIMIT + SPILL_NOTE_SLACK : TOOL_RESULT_LIMIT;
312
397
  try {
313
398
  const result = await tool.run(args, signal); // await covers both sync and async tools
314
399
  return result.slice(0, cap); // cap the result size
package/dist/ui.js CHANGED
@@ -28,7 +28,7 @@ export function banner(version, model, host) {
28
28
  { text: `mini-agent v${version}`, style: chalk.bold.cyan },
29
29
  { text: `${model} · ${host}`, style: chalk.dim },
30
30
  { text: "" },
31
- { text: "/help commands @file attach a file Ctrl+C interrupt", style: chalk.dim },
31
+ { text: "/help commands @file attach Esc interrupt Ctrl+C quit", style: chalk.dim },
32
32
  ]);
33
33
  }
34
34
  // The input prompt. A styled chevron, not a bare ">". Plan mode (Day 20) gets a
@@ -48,6 +48,10 @@ export const mark = {
48
48
  hookBlock: (event) => chalk.red(` ⎿ blocked by ${event} hook`),
49
49
  subAgentStart: (desc, tier) => chalk.blue(" ⎿ sub-agent") + (tier ? chalk.dim(` [${tier}]`) : "") + chalk.blue(`: ${desc}`),
50
50
  subAgentDone: chalk.blue(" ⎿ sub-agent done"),
51
+ bgNote: (n) => chalk.magenta(" ⎿ ") + chalk.magenta(`${n} background task${n === 1 ? "" : "s"} finished — notified the agent`), // Day 37
52
+ teammateStart: (name, role) => chalk.magenta(" ⎿ teammate ") + chalk.bold(name) + chalk.magenta(` spawned`) + chalk.dim(` — ${role}`), // Day 38
53
+ teammateDone: (name, ok) => (ok ? chalk.magenta(" ⎿ teammate ") + chalk.bold(name) + chalk.magenta(" finished") : chalk.yellow(" ⎿ teammate ") + chalk.bold(name) + chalk.yellow(" ended (not done)")),
54
+ inbox: (n) => chalk.magenta(" ⎿ ") + chalk.magenta(`${n} team message${n === 1 ? "" : "s"} received`),
51
55
  note: (s) => chalk.dim(s), // dim asides (resumed, attached, cleared…)
52
56
  warn: (s) => chalk.yellow(s),
53
57
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-from-zero",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Build a Claude Code-style CLI agent from scratch, with AI as your pair programmer — companion code for a 10-day tutorial (DeepSeek by default, any OpenAI-compatible model)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "eval": "tsx --env-file=.env eval/run.ts",
17
17
  "build": "tsc -p tsconfig.build.json",
18
18
  "prepare": "npm run build",
19
- "test": "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/memory.test.ts && tsx tests/cost.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/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/ui.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.test.ts && tsx tests/skills.test.ts"
19
+ "test": "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/memory.test.ts && tsx tests/cost.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/changes.test.ts && tsx tests/mentions.test.ts && tsx tests/session.test.ts && tsx tests/ui.test.ts && tsx tests/approval.test.ts && tsx tests/form.test.ts && tsx tests/model.test.ts && tsx tests/reasoning.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/board.test.ts"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=20.6"
@@ -24,8 +24,12 @@
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
26
  "chalk": "^5.6.2",
27
+ "cli-table3": "^0.6.5",
28
+ "marked": "^15.0.12",
29
+ "marked-terminal": "^7.3.0",
27
30
  "openai": "^6.42.0",
28
- "ora": "^9.4.0"
31
+ "ora": "^9.4.0",
32
+ "wrap-ansi": "^10.0.0"
29
33
  },
30
34
  "devDependencies": {
31
35
  "@types/node": "^25.9.2",