agent-from-zero 0.32.0 → 0.32.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/agent.js +2 -1
- package/dist/ink/app.js +85 -2
- package/dist/ink/setup.js +2 -0
- package/dist/loop.js +94 -31
- package/dist/ui.js +14 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -4,7 +4,7 @@ import chalk from "chalk"; // terminal colors
|
|
|
4
4
|
import readline from "node:readline/promises"; // promise-based terminal input
|
|
5
5
|
import { createRequire } from "node:module"; // to read package.json for --version
|
|
6
6
|
import { CONFIG, requireApiKey, saveGlobalSetting, PROJECT_SETTINGS_PATH, GLOBAL_SETTINGS_PATH } from "./config.js"; // provider-agnostic settings (.env loaded there)
|
|
7
|
-
import { runLoop, TerminateReason, MAX_RETRIES } from "./loop.js"; // the state machine
|
|
7
|
+
import { runLoop, TerminateReason, MAX_RETRIES, killAllSubAgents } from "./loop.js"; // the state machine
|
|
8
8
|
import { buildSystemMessage } from "./prompt.js"; // the constitution + optional AGENT.md project memory
|
|
9
9
|
import { forgetFilesExcept, registerExternalTool } from "./tools.js"; // file-state reset + tool registration
|
|
10
10
|
import { compactHistory, estimateHistoryTokens, COMPACT_AT } from "./context.js"; // for the manual /compact command
|
|
@@ -155,6 +155,7 @@ async function main() {
|
|
|
155
155
|
const disconnectMcp = await connectMcpServers();
|
|
156
156
|
process.on("exit", disconnectMcp); // best-effort cleanup of server subprocesses
|
|
157
157
|
process.on("exit", killAllBackground); // Day 37: SIGKILL any background job (dev server, slow install) so it never outlives the agent as an orphan
|
|
158
|
+
process.on("exit", killAllSubAgents); // mark any still-running sub-agents as killed so they don't leave pending promises
|
|
158
159
|
// The optional LLM permission judge, built once if a settings file enabled it.
|
|
159
160
|
const judge = CONFIG.judge.enabled ? new Judge(client, CONFIG.judge.model || CONFIG.model) : undefined;
|
|
160
161
|
if (judge)
|
package/dist/ink/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { formatElapsed, renderMenu, MENU_HINT, formatModelChoices } from "../ui.
|
|
|
6
6
|
import { renderMarkdown } from "../markdown.js"; // model speaks markdown → ANSI, same as the non-Ink REPL
|
|
7
7
|
import { initFormState, reduceForm, renderForm, collectAnswers } from "../form.js"; // the ask_user form: pure state machine + renderer
|
|
8
8
|
import { CONFIG, saveGlobalSetting } from "../config.js"; // session allowlist + /model save
|
|
9
|
-
import { TerminateReason } from "../loop.js"; // how a turn can end
|
|
9
|
+
import { TerminateReason, listSubAgents } from "../loop.js"; // how a turn can end
|
|
10
10
|
import { compactHistory, COMPACT_AT } from "../context.js"; // /compact
|
|
11
11
|
import { forgetFilesExcept } from "../tools.js"; // /clear resets the file read-state
|
|
12
12
|
import { clearUndo } from "../undo.js";
|
|
@@ -27,6 +27,25 @@ import { runInfoCommand } from "./commands.js"; // the non-interactive slash com
|
|
|
27
27
|
// Injected when plan mode turns on, so the model knows the rules it now lives in.
|
|
28
28
|
const PLAN_MODE_NOTICE = `[plan mode ON] Investigate this request using only read-only tools — read_file, search, and safe read-only shell (ls, cat, git status). Do NOT write files, edit, or run mutating commands; the permission gate will block them. When you have a concrete, ordered plan, call the exit_plan_mode tool with that plan. The user reviews and approves it before you make any change.`;
|
|
29
29
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // the braille spinner ora uses, hand-rolled for Ink
|
|
30
|
+
// Cap the live streaming preview to a terminal-aware tail. THIS IS LOAD-BEARING:
|
|
31
|
+
// Ink redraws the whole dynamic region (everything below <Static>) on every
|
|
32
|
+
// token, and when that region grows TALLER than the terminal, Ink can no longer
|
|
33
|
+
// erase its previous frame — committed lines get reprinted (duplicate messages)
|
|
34
|
+
// and the cursor math drifts (sudden blank gaps). So we never let the in-flight
|
|
35
|
+
// answer render more than the rows we can spare; the FULL reply still commits to
|
|
36
|
+
// <Static> (formatted as markdown) the moment it finishes.
|
|
37
|
+
function liveTail(s) {
|
|
38
|
+
const rows = process.stdout.rows || 24;
|
|
39
|
+
const maxLines = Math.max(3, rows - 14); // reserve rows for the input box + status bar + agent bar + margins
|
|
40
|
+
const cols = process.stdout.columns || 80;
|
|
41
|
+
const maxChars = maxLines * cols; // rough bound so one very long line can't blow past either
|
|
42
|
+
let t = s.length > maxChars ? s.slice(-maxChars) : s;
|
|
43
|
+
const lines = t.split("\n");
|
|
44
|
+
const clipped = s.length > maxChars || lines.length > maxLines;
|
|
45
|
+
if (lines.length > maxLines)
|
|
46
|
+
t = lines.slice(-maxLines).join("\n");
|
|
47
|
+
return clipped ? "… " + t : t; // a leading ellipsis signals there's more above (it's all in the scrollback on commit)
|
|
48
|
+
}
|
|
30
49
|
function Spinner() {
|
|
31
50
|
const [f, setF] = useState(0);
|
|
32
51
|
useEffect(() => {
|
|
@@ -83,6 +102,8 @@ export function App({ session, runTurn }) {
|
|
|
83
102
|
const [sessionId, setSessionId] = useState(session.initialSessionId); // changes on /clear, /resume
|
|
84
103
|
const [planMode, setPlan] = useState(isPlanMode()); // mirrored into the prompt frame
|
|
85
104
|
const [histIdx, setHistIdx] = useState(null); // ↑/↓ recall position (null = editing a fresh line)
|
|
105
|
+
const [subAgentFocus, setSubAgentFocus] = useState(null); // which sub-agent is highlighted (null = none)
|
|
106
|
+
const [subAgentDetail, setSubAgentDetail] = useState(null); // the id of the sub-agent whose output is shown in detail
|
|
86
107
|
const [, setTick] = useState(0); // forces a re-render once a second so the clock / cost tick
|
|
87
108
|
const { exit } = useApp();
|
|
88
109
|
const pushItem = useRef((it) => setItems((xs) => [...xs, it])).current;
|
|
@@ -338,6 +359,12 @@ export function App({ session, runTurn }) {
|
|
|
338
359
|
return note(getReasoning() ?? chalk.dim("(no thinking recorded for the last answer)"));
|
|
339
360
|
if (key.ctrl && char === "t")
|
|
340
361
|
return note(getToolCalls() ?? chalk.dim("(no tool calls recorded for the last answer)"));
|
|
362
|
+
// Esc while a sub-agent detail is open: close it, don't interrupt.
|
|
363
|
+
if (key.escape && subAgentDetail && !pending) {
|
|
364
|
+
setSubAgentDetail(null);
|
|
365
|
+
setSubAgentFocus(null);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
341
368
|
// Esc while a turn runs (and no menu is up): interrupt it — like the first
|
|
342
369
|
// Ctrl+C of the readline REPL, without the force-quit escalation.
|
|
343
370
|
if (key.escape && busy && !pending) {
|
|
@@ -386,9 +413,53 @@ export function App({ session, runTurn }) {
|
|
|
386
413
|
}
|
|
387
414
|
return;
|
|
388
415
|
}
|
|
416
|
+
// Tab / Shift+Tab: cycle through the sub-agent list below the input box.
|
|
417
|
+
// Enter on a highlighted sub-agent toggles its detail view.
|
|
418
|
+
const agents = listSubAgents();
|
|
419
|
+
if (agents.length > 0 && !key.return && key.tab) {
|
|
420
|
+
const back = key.shift; // Shift+Tab = go backwards
|
|
421
|
+
if (subAgentDetail) {
|
|
422
|
+
// Detail open: Tab moves to next + closes detail.
|
|
423
|
+
setSubAgentDetail(null);
|
|
424
|
+
setSubAgentFocus((prev) => {
|
|
425
|
+
const n = agents.length;
|
|
426
|
+
if (back) {
|
|
427
|
+
const next = prev === null ? n - 1 : (prev - 1 + n) % n;
|
|
428
|
+
return next;
|
|
429
|
+
}
|
|
430
|
+
const next = prev === null ? 0 : (prev + 1) % n;
|
|
431
|
+
return next;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
setSubAgentDetail(null);
|
|
436
|
+
setSubAgentFocus((prev) => {
|
|
437
|
+
const n = agents.length;
|
|
438
|
+
if (back) {
|
|
439
|
+
if (prev === null)
|
|
440
|
+
return n - 1;
|
|
441
|
+
const next = (prev - 1 + n) % n;
|
|
442
|
+
return next === n - 1 ? null : next;
|
|
443
|
+
}
|
|
444
|
+
if (prev === null)
|
|
445
|
+
return 0;
|
|
446
|
+
const next = (prev + 1) % n;
|
|
447
|
+
return next === 0 ? null : next;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Enter with a sub-agent highlighted: show its detail.
|
|
453
|
+
if (agents.length > 0 && subAgentFocus !== null && key.return) {
|
|
454
|
+
setSubAgentDetail(agents[subAgentFocus].id === subAgentDetail ? null : agents[subAgentFocus].id);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
389
457
|
if (busy)
|
|
390
458
|
return; // a turn in flight with no prompt — ignore typing for now (type-ahead is later)
|
|
391
459
|
if (key.return) {
|
|
460
|
+
// Dismiss the sub-agent detail/focus on a normal submit.
|
|
461
|
+
setSubAgentDetail(null);
|
|
462
|
+
setSubAgentFocus(null);
|
|
392
463
|
const text = input.trim();
|
|
393
464
|
setInput("");
|
|
394
465
|
setHistIdx(null);
|
|
@@ -431,5 +502,17 @@ export function App({ session, runTurn }) {
|
|
|
431
502
|
setInput((s) => s + add);
|
|
432
503
|
}
|
|
433
504
|
});
|
|
434
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), live !== null && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : live }) })] })), status !== null && (_jsxs(Box, { marginTop: live === null ? 1 : 0, children: [_jsx(Spinner, {}), _jsxs(Text, { children: [" ", status] })] })), pending?.kind === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: pending.header }), _jsx(Text, { children: renderMenu(pending.options, menuSel) }), _jsx(Text, { children: MENU_HINT })] })), pending?.kind === "form" && formState && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: renderForm(pending.questions, formState) }) })), _jsxs(Box, { borderStyle: "round", borderColor: planMode ? "magenta" : "cyan", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: planMode ? "magenta" : "cyan", children: planMode ? "plan ❯ " : "❯ " }), _jsx(Text, { children: input }), _jsx(Text, { inverse: true, children: " " })] }),
|
|
505
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: items, children: (item, i) => _jsx(ItemView, { item: item }, i) }), live !== null && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { color: "green", children: "\u23FA " }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: live === "" ? chalk.dim("…") : liveTail(live) }) })] })), (status !== null || (busy && live === null)) && (_jsxs(Box, { marginTop: live === null ? 1 : 0, children: [_jsx(Spinner, {}), _jsxs(Text, { children: [" ", status ?? chalk.dim("working…")] })] })), pending?.kind === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", children: pending.header }), _jsx(Text, { children: renderMenu(pending.options, menuSel) }), _jsx(Text, { children: MENU_HINT })] })), pending?.kind === "form" && formState && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: renderForm(pending.questions, formState) }) })), _jsxs(Box, { borderStyle: "round", borderColor: planMode ? "magenta" : "cyan", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: planMode ? "magenta" : "cyan", children: planMode ? "plan ❯ " : "❯ " }), _jsx(Text, { children: input }), _jsx(Text, { inverse: true, children: " " })] }), (() => {
|
|
506
|
+
const agents = listSubAgents();
|
|
507
|
+
if (!agents.length)
|
|
508
|
+
return null;
|
|
509
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "agents \u00B7 " }), agents.map((sa, i) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: i === subAgentFocus ? "cyan" : "blue", bold: i === subAgentFocus, children: sa.id }), _jsx(Text, { dimColor: true, children: sa.status === "running" ? " ●" : sa.status === "done" ? " ✓" : " ✗" }), i < agents.length - 1 && _jsx(Text, { dimColor: true, children: " \u2502 " })] }, sa.id))), _jsx(Text, { dimColor: true, children: " \u00B7 Tab" })] }), subAgentDetail && (() => {
|
|
510
|
+
const detail = agents.find((sa) => sa.id === subAgentDetail);
|
|
511
|
+
if (!detail)
|
|
512
|
+
return null;
|
|
513
|
+
const head = `${detail.id} · ${detail.status === "running" ? "running" : detail.status === "done" ? "done" : "failed"}`;
|
|
514
|
+
const body = detail.result ?? "(still working…)";
|
|
515
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 0, borderStyle: "single", borderColor: "blue", paddingX: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: head }), _jsx(Text, { dimColor: true, children: detail.description }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: body.length > 500 ? body.slice(0, 500) + "\n…" : body }) })] }));
|
|
516
|
+
})()] }));
|
|
517
|
+
})(), _jsx(StatusBar, { model: model, dir: dir, branch: branch, status: getStatus() })] }));
|
|
435
518
|
}
|
package/dist/ink/setup.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerExternalTool } from "../tools.js";
|
|
|
15
15
|
import { rememberTool, readMemory } from "../memory.js";
|
|
16
16
|
import { loadSkills, buildSkillTool } from "../skills.js";
|
|
17
17
|
import { killAllBackground } from "../background.js";
|
|
18
|
+
import { killAllSubAgents } from "../loop.js";
|
|
18
19
|
import { banner } from "../ui.js";
|
|
19
20
|
// The Ink REPL's session bootstrap — the same wiring agent.ts (the readline REPL)
|
|
20
21
|
// does before the prompt loop, but front-end-agnostic: it touches none of
|
|
@@ -51,6 +52,7 @@ export async function buildInkSession(opts = {}) {
|
|
|
51
52
|
const disconnectMcp = await connectMcpServers();
|
|
52
53
|
process.on("exit", disconnectMcp);
|
|
53
54
|
process.on("exit", killAllBackground); // SIGKILL any background job so it never outlives the agent
|
|
55
|
+
process.on("exit", killAllSubAgents); // mark any still-running sub-agents as killed
|
|
54
56
|
const judge = CONFIG.judge.enabled ? new Judge(client, CONFIG.judge.model || CONFIG.model) : undefined;
|
|
55
57
|
if (judge)
|
|
56
58
|
notices.push(`(permission judge on — model ${CONFIG.judge.model || CONFIG.model})`);
|
package/dist/loop.js
CHANGED
|
@@ -55,6 +55,7 @@ const taskTool = {
|
|
|
55
55
|
description: `Delegate one self-contained subtask to a sub-agent that works in a FRESH context.
|
|
56
56
|
The sub-agent has the same tools (except task) but knows NOTHING about this conversation — put every detail it needs into the description.
|
|
57
57
|
Use it for exploration that would flood your context: reading many files, broad searches, summarizing a directory.
|
|
58
|
+
The sub-agent runs ASYNC — it returns a handle immediately and you are notified when it finishes. Do NOT wait idly: continue working or wait for its result.
|
|
58
59
|
The sub-agent's report is INPUT MATERIAL, not verified truth — re-check key claims yourself before acting on them.
|
|
59
60
|
On error: a failed sub-agent returns [sub-agent failed: reason] — retry with a clearer description, or do the work yourself.`,
|
|
60
61
|
parameters: {
|
|
@@ -277,37 +278,94 @@ function toolsFor(opts) {
|
|
|
277
278
|
// watching the board (it lays out tasks; teammates pull from it).
|
|
278
279
|
return [...builtins, taskTool, askUserTool, spawnTeammateTool, sendMessageTool, requestShutdownTool, requestPlanTool, reviewPlanTool, createTaskTool, listTasksTool];
|
|
279
280
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
const pendingSubAgents = new Map();
|
|
282
|
+
let subAgentSeq = 0;
|
|
283
|
+
// Collect finished sub-agent results (like pendingNotifications for background
|
|
284
|
+
// tasks). Each result surfaces exactly once (the `notified` flag).
|
|
285
|
+
function pendingSubAgentResults() {
|
|
286
|
+
const done = [];
|
|
287
|
+
for (const sa of pendingSubAgents.values()) {
|
|
288
|
+
if (sa.result === null || sa.notified)
|
|
289
|
+
continue;
|
|
290
|
+
sa.notified = true;
|
|
291
|
+
done.push(`<subagent_notification>\n <id>${sa.id}</id>\n <task>${sa.description.slice(0, 200)}</task>\n <result>\n${sa.result}\n </result>\n</subagent_notification>`);
|
|
292
|
+
}
|
|
293
|
+
if (!done.length)
|
|
294
|
+
return "";
|
|
295
|
+
return `A sub-agent you delegated work to has finished. Read its report carefully — it is INPUT MATERIAL, not verified truth. Verify key claims yourself, then continue.\n\n${done.join("\n\n")}`;
|
|
296
|
+
}
|
|
297
|
+
// Inject finished sub-agent results into the conversation, like
|
|
298
|
+
// injectBackgroundNotifications. Returns true if something was injected.
|
|
299
|
+
function injectSubAgentResults(messages, opts) {
|
|
300
|
+
if (opts.subAgent)
|
|
301
|
+
return false; // notifications belong to the top-level conversation
|
|
302
|
+
const note = pendingSubAgentResults();
|
|
303
|
+
if (!note)
|
|
304
|
+
return false;
|
|
305
|
+
messages.push({ role: "user", content: note });
|
|
287
306
|
if (!opts.quiet)
|
|
288
|
-
sink(opts).note(mark.
|
|
289
|
-
|
|
307
|
+
sink(opts).note(mark.subAgentDone);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
// Start a sub-agent WITHOUT awaiting — returns a handle immediately. The
|
|
311
|
+
// sub-agent's result will be injected as a notification when it finishes.
|
|
312
|
+
function spawnSubAgent(description, opts) {
|
|
313
|
+
emit("agent_subagent_spawn");
|
|
314
|
+
const subModel = subAgentModelFor(opts);
|
|
315
|
+
const id = `sa_${++subAgentSeq}`;
|
|
316
|
+
if (!opts.quiet)
|
|
317
|
+
sink(opts).note(mark.subAgentStart(description.slice(0, 100), subModel !== opts.model ? subModel : ""));
|
|
318
|
+
runHooks("SubagentStart", { description: description.slice(0, 200), model: subModel }); // fire-and-forget (observational)
|
|
290
319
|
const snapshot = snapshotFileState(); // what the sub-agent reads, the parent has NOT seen
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
320
|
+
const sa = { id, description, done: Promise.resolve(), result: null, notified: false };
|
|
321
|
+
sa.done = (async () => {
|
|
322
|
+
try {
|
|
323
|
+
const subMessages = [
|
|
324
|
+
{ role: "system", content: SUB_AGENT_PROMPT },
|
|
325
|
+
{ role: "user", content: description },
|
|
326
|
+
];
|
|
327
|
+
const result = await runLoop(subMessages, { ...opts, subAgent: true, model: subModel });
|
|
328
|
+
if (result.reason === TerminateReason.Done && result.finalText?.trim()) {
|
|
329
|
+
sa.result = `Sub-agent report (INPUT MATERIAL — verify key claims before acting on them):\n${result.finalText}`;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
sa.result = `[sub-agent failed: ${result.reason}]`;
|
|
333
|
+
}
|
|
301
334
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
catch (err) {
|
|
336
|
+
sa.result = `[sub-agent crashed: ${err.message}]`;
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
restoreFileState(snapshot);
|
|
340
|
+
await runHooks("SubagentStop", { description: description.slice(0, 200) });
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
pendingSubAgents.set(id, sa);
|
|
344
|
+
return `Sub-agent started (${id}): "${description.slice(0, 100)}". Continue working — you'll be notified when it finishes. Do NOT wait idly; keep working on other parts of the task, or wait for the notification.`;
|
|
345
|
+
}
|
|
346
|
+
// True if any sub-agent is still running.
|
|
347
|
+
function hasRunningSubAgents() {
|
|
348
|
+
return [...pendingSubAgents.values()].some((sa) => sa.result === null);
|
|
349
|
+
}
|
|
350
|
+
// Kill every still-running sub-agent. Called on session exit.
|
|
351
|
+
export function killAllSubAgents() {
|
|
352
|
+
for (const sa of pendingSubAgents.values()) {
|
|
353
|
+
if (sa.result === null)
|
|
354
|
+
sa.result = "[sub-agent killed: session exited]";
|
|
309
355
|
}
|
|
310
356
|
}
|
|
357
|
+
// Snapshot of current sub-agents for the UI (Ink app shows a list below the
|
|
358
|
+
// input box, like Claude Code's agent list). Newest first.
|
|
359
|
+
export function listSubAgents() {
|
|
360
|
+
return [...pendingSubAgents.values()]
|
|
361
|
+
.reverse()
|
|
362
|
+
.map((sa) => ({
|
|
363
|
+
id: sa.id,
|
|
364
|
+
description: sa.description.slice(0, 80),
|
|
365
|
+
status: sa.result === null ? "running" : sa.result.startsWith("[sub-agent failed") || sa.result.startsWith("[sub-agent crashed") || sa.result.startsWith("[sub-agent killed") ? "failed" : "done",
|
|
366
|
+
result: sa.result ?? undefined,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
311
369
|
// Exponential backoff with jitter: 500ms, 1s, 2s, ... ±25%, capped.
|
|
312
370
|
// Without jitter, every client that failed at the same second retries at the
|
|
313
371
|
// same second — a thundering herd against an already-struggling server.
|
|
@@ -613,7 +671,7 @@ async function execute(call, opts) {
|
|
|
613
671
|
}
|
|
614
672
|
if (!description)
|
|
615
673
|
return "[error] task requires a non-empty description argument.";
|
|
616
|
-
return
|
|
674
|
+
return spawnSubAgent(description, opts); // spawn the worker (async — returns immediately; result injected later)
|
|
617
675
|
}
|
|
618
676
|
// The sender's mailbox name: a teammate sends as itself, everyone else as "lead".
|
|
619
677
|
const senderName = (opts) => opts.teammate?.name ?? LEAD;
|
|
@@ -1190,12 +1248,14 @@ opts) {
|
|
|
1190
1248
|
break; // exit the inner while → advance the round counter
|
|
1191
1249
|
}
|
|
1192
1250
|
}
|
|
1193
|
-
// A background task may have finished in the very round the
|
|
1194
|
-
// to stop. Don't return with an unread result on the table:
|
|
1195
|
-
// run one more round so the model can react. Each
|
|
1196
|
-
// this adds at most one round per finished
|
|
1251
|
+
// A background task (or sub-agent) may have finished in the very round the
|
|
1252
|
+
// model decided to stop. Don't return with an unread result on the table:
|
|
1253
|
+
// surface it and run one more round so the model can react. Each notifies
|
|
1254
|
+
// once, so this adds at most one round per finished job — never infinite.
|
|
1197
1255
|
if (injectBackgroundNotifications(messages, opts))
|
|
1198
1256
|
break; // exit the inner while → advance the round counter
|
|
1257
|
+
if (injectSubAgentResults(messages, opts))
|
|
1258
|
+
break;
|
|
1199
1259
|
// Agent teams (Day 38/39): the lead waits for teammate messages and
|
|
1200
1260
|
// reacts; a teammate idle-loops for new work and leaves only on the
|
|
1201
1261
|
// shutdown handshake (or its idle backstop). If there's something to
|
|
@@ -1240,6 +1300,9 @@ opts) {
|
|
|
1240
1300
|
// the model sees the outcome on the next round without ever having blocked
|
|
1241
1301
|
// on it. Tasks that are still running stay silent until they finish.
|
|
1242
1302
|
injectBackgroundNotifications(messages, opts);
|
|
1303
|
+
// Sub-agent results (async task delegation): same pattern — a finished
|
|
1304
|
+
// sub-agent's report is injected here so the model can react next round.
|
|
1305
|
+
injectSubAgentResults(messages, opts);
|
|
1243
1306
|
// Agent teams (Day 38/39): drain any messages that arrived during this
|
|
1244
1307
|
// round (results, hand-offs, protocol requests/responses) so the agent
|
|
1245
1308
|
// reacts to them next round. A shutdown_request seen here just flags the
|
package/dist/ui.js
CHANGED
|
@@ -23,14 +23,27 @@ function box(lines) {
|
|
|
23
23
|
});
|
|
24
24
|
return [top, ...body, bottom].join("\n");
|
|
25
25
|
}
|
|
26
|
+
// The llama — our mascot. Drawn in ASCII so it renders in any terminal.
|
|
27
|
+
// Each line is styled with a warm alpaca-wool tone (ansi256 #215, a sandy gold).
|
|
28
|
+
const LLAMA = [
|
|
29
|
+
" ╱▔▔╲ ╱▔▔╲",
|
|
30
|
+
" ╱ ╲_╱ ╲",
|
|
31
|
+
" ╭──────────────╮",
|
|
32
|
+
" │ ◉ ◉ │",
|
|
33
|
+
" │ ⏝ │",
|
|
34
|
+
" ╰──────┬──────╯",
|
|
35
|
+
" │",
|
|
36
|
+
].join("\n");
|
|
26
37
|
// The welcome banner, shown once at startup.
|
|
27
38
|
export function banner(version, model, host) {
|
|
28
|
-
|
|
39
|
+
const art = chalk.ansi256(215)(LLAMA);
|
|
40
|
+
const info = box([
|
|
29
41
|
{ text: `mini-agent v${version}`, style: chalk.bold.cyan },
|
|
30
42
|
{ text: `${model} · ${host}`, style: chalk.dim },
|
|
31
43
|
{ text: "" },
|
|
32
44
|
{ text: "/help commands @file attach Esc interrupt Ctrl+C quit", style: chalk.dim },
|
|
33
45
|
]);
|
|
46
|
+
return art + "\n" + info;
|
|
34
47
|
}
|
|
35
48
|
// The input prompt. A styled chevron, not a bare ">". Plan mode (Day 20) gets a
|
|
36
49
|
// distinct marker so you always know writes are blocked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-from-zero",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.1",
|
|
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": {
|