agent-from-zero 0.35.1 → 0.35.3
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 +141 -0
- package/dist/agent.js +43 -26
- package/dist/completion.js +93 -3
- package/dist/config.js +38 -1
- package/dist/context.js +46 -6
- package/dist/ink/app.js +158 -30
- package/dist/ink/chat.js +3 -1
- package/dist/ink/commands.js +6 -3
- package/dist/ink/completion.js +13 -5
- package/dist/ink/setup.js +16 -15
- package/dist/ink/skills-panel.js +86 -0
- package/dist/loop.js +34 -8
- package/dist/mcp.js +332 -16
- package/dist/skills.js +282 -41
- package/dist/ui.js +8 -1
- package/package.json +2 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { nextSkillMode, nextSkillSort, setSkillMode, skillListingTokens, skillLocked, skillMode, skillPanelRows, skillSource } from "../skills.js";
|
|
5
|
+
// /skills — Claude Code's skill manager. One row per skill on disk:
|
|
6
|
+
// ❯ ✔ on grill-me · user · ~20 tok
|
|
7
|
+
// 🔒 user-only deploy · project · ~30 tok · locked by author
|
|
8
|
+
// Enter/space cycles on → user-only → off (saved at once, live on the next
|
|
9
|
+
// model call), "/" searches, "t" changes the sort, Esc closes. The panel owns
|
|
10
|
+
// its keys while it is open; the app's main handler stands aside.
|
|
11
|
+
const LAVENDER = "#B1B9F9";
|
|
12
|
+
const SORT_LABEL = { name: "name", tokens: "tokens", source: "source" };
|
|
13
|
+
function ModeCell({ mode, locked }) {
|
|
14
|
+
if (locked)
|
|
15
|
+
return _jsx(Text, { dimColor: true, children: "🔒 user-only " });
|
|
16
|
+
if (mode === "on")
|
|
17
|
+
return _jsx(Text, { color: "green", children: "✔ on " });
|
|
18
|
+
if (mode === "user-only")
|
|
19
|
+
return _jsx(Text, { color: "yellow", children: "◐ user-only " });
|
|
20
|
+
return _jsx(Text, { dimColor: true, children: "○ off " });
|
|
21
|
+
}
|
|
22
|
+
export function SkillsPanel({ skills, maxRows, onClose }) {
|
|
23
|
+
const [cursor, setCursor] = useState(0);
|
|
24
|
+
const [query, setQuery] = useState("");
|
|
25
|
+
const [searching, setSearching] = useState(false);
|
|
26
|
+
const [sort, setSort] = useState("name");
|
|
27
|
+
const [, setVersion] = useState(0); // re-render after a mode change (modes live in CONFIG)
|
|
28
|
+
const initial = useRef(new Map(skills.map((s) => [s.name, skillMode(s)]))).current; // for the summary on close
|
|
29
|
+
const rows = skillPanelRows(skills, query, sort);
|
|
30
|
+
const at = Math.min(cursor, Math.max(0, rows.length - 1));
|
|
31
|
+
useInput((char, key) => {
|
|
32
|
+
if (searching) {
|
|
33
|
+
if (key.escape || key.return) {
|
|
34
|
+
setSearching(false);
|
|
35
|
+
if (key.escape)
|
|
36
|
+
setQuery("");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.backspace || key.delete) {
|
|
40
|
+
setQuery((q) => q.slice(0, -1));
|
|
41
|
+
setCursor(0);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (key.upArrow || key.downArrow) {
|
|
45
|
+
setSearching(false);
|
|
46
|
+
} // fall through to navigation
|
|
47
|
+
else if (char === "/")
|
|
48
|
+
return; // skill names never contain "/": a second "/" press must not become the query
|
|
49
|
+
else if (char && !key.ctrl && !key.meta) {
|
|
50
|
+
setQuery((q) => q + char);
|
|
51
|
+
setCursor(0);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.escape) {
|
|
58
|
+
const changes = skills.filter((s) => initial.get(s.name) !== skillMode(s)).map((s) => `${s.name} → ${skillMode(s)}`);
|
|
59
|
+
return onClose(changes.length ? `Skills updated: ${changes.join(", ")}` : "No changes");
|
|
60
|
+
}
|
|
61
|
+
if (key.upArrow)
|
|
62
|
+
return setCursor((c) => (rows.length ? (Math.min(c, rows.length - 1) - 1 + rows.length) % rows.length : 0));
|
|
63
|
+
if (key.downArrow)
|
|
64
|
+
return setCursor((c) => (rows.length ? (Math.min(c, rows.length - 1) + 1) % rows.length : 0));
|
|
65
|
+
if (char === "/")
|
|
66
|
+
return setSearching(true);
|
|
67
|
+
if (char === "t")
|
|
68
|
+
return setSort((s) => nextSkillSort(s));
|
|
69
|
+
if (key.return || char === " ") {
|
|
70
|
+
const s = rows[at];
|
|
71
|
+
if (!s || skillLocked(s))
|
|
72
|
+
return; // the author pinned it — nothing to cycle
|
|
73
|
+
setSkillMode(s.name, nextSkillMode(skillMode(s)));
|
|
74
|
+
setVersion((v) => v + 1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// A window of rows around the cursor, so a long list never outgrows the terminal.
|
|
78
|
+
const count = Math.max(3, Math.min(rows.length, maxRows));
|
|
79
|
+
const start = Math.max(0, Math.min(at - Math.floor(count / 2), rows.length - count));
|
|
80
|
+
const shown = rows.slice(start, start + count);
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: LAVENDER, children: "Skills" }), _jsxs(Text, { dimColor: true, children: [skills.length, " skill", skills.length === 1 ? "" : "s", " \u00B7 ", searching ? "type to filter, ↑↓ to pick, Enter/Esc when done" : `enter/space to cycle, / to search, t to sort (${SORT_LABEL[sort]}), Esc to close`] }), _jsxs(Box, { borderStyle: "round", borderColor: searching ? LAVENDER : "gray", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: searching ? LAVENDER : undefined, dimColor: !searching, children: "Search: " }), _jsx(Text, { dimColor: !query && !searching, children: query || (searching ? "" : "press / to search skills…") }), searching && _jsx(Text, { inverse: true, children: " " })] }), start > 0 && _jsxs(Text, { dimColor: true, children: [" \u2191 ", start, " more above"] }), shown.map((s, i) => {
|
|
82
|
+
const selected = start + i === at;
|
|
83
|
+
const locked = skillLocked(s);
|
|
84
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: LAVENDER, children: selected ? "❯ " : " " }), _jsx(ModeCell, { mode: skillMode(s), locked: locked }), _jsxs(Text, { bold: !selected, color: selected ? LAVENDER : undefined, children: [" ", s.name] }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", skillSource(s), " \u00B7 ~", skillListingTokens(s), " tok", locked ? " · locked by author" : ""] })] }, s.path || s.name));
|
|
85
|
+
}), !rows.length && _jsxs(Text, { dimColor: true, children: [" ", skills.length ? `No skills match "${query}"` : "No skills — add one at .mini-agent/skills/<name>/SKILL.md or ~/.config/mini-agent/skills/"] }), start + count < rows.length && _jsxs(Text, { dimColor: true, children: [" \u2193 ", rows.length - start - count, " more below"] })] }));
|
|
86
|
+
}
|
package/dist/loop.js
CHANGED
|
@@ -5,7 +5,7 @@ import { toolDefinitions, dispatch, snapshotFileState, restoreFileState, isReadO
|
|
|
5
5
|
import { classifyError, ApiErrorKind } from "./errors.js"; // failure taxonomy
|
|
6
6
|
import { checkPermission } from "./permissions.js"; // the allow/ask/deny gate
|
|
7
7
|
import { previewChange } from "./diff.js"; // show the diff before a write so approval is informed
|
|
8
|
-
import {
|
|
8
|
+
import { contextTokens, compactHistory, compactThreshold, recordContextUsage, MAX_COMPACTIONS_PER_QUERY, MAX_COMPACT_FAILURES } from "./context.js"; // context management
|
|
9
9
|
import { SUB_AGENT_PROMPT, TEAMMATE_PROMPT } from "./prompt.js"; // the sub-agent + teammate constitutions
|
|
10
10
|
import { LEAD, MAX_TEAMMATES, sendMessage, sendProtocol, readInbox, inboxCount, registerTeammate, finishTeammate, teammateExists, teammateCount, createRequest, resolveResponse, setTeammateState, anyTeammateBusy, runningTeammates, markShutdown, shutdownRequestId, resetTeam, listTeammateViews } from "./team.js"; // agent teams (Day 38) + team protocols (Day 39): mailboxes, registry, request/response contracts
|
|
11
11
|
import { createTask, claimTask, completeTask, claimNextAvailable, boardSummary, resetBoard } from "./board.js"; // the shared task board (Day 40): autonomous work claiming
|
|
@@ -18,6 +18,7 @@ import { mark, thinkingWord, spinnerText } from "./ui.js"; // centralized termin
|
|
|
18
18
|
import { recordToolCall, recordToolResult, recordToolDetail, recordReasoning } from "./tui.js"; // folded tool-call trace (Ctrl+T) + the model's hidden thinking (Ctrl+R)
|
|
19
19
|
import { STDOUT_OUTPUT } from "./output.js"; // where the loop's screen output goes (stdout by default, Ink REPL passes its own)
|
|
20
20
|
import { todoNag, getTodos, renderTodos } from "./todos.js"; // the agent's plan: show it on screen + nag when it goes stale
|
|
21
|
+
import { currentSkills, skillBodyMessages, skillListingReminder, syncSkillTool } from "./skills.js"; // skill listing reminders + launched skill bodies
|
|
21
22
|
import { pendingNotifications } from "./background.js"; // background tasks (Day 37): surface finished jobs as a turn
|
|
22
23
|
import { consumeCronQueue, cronItemsPending, cronTriggerContent } from "./cron.js"; // cron scheduler (Day s14): scheduled, recurring work
|
|
23
24
|
export const MAX_RETRIES = 10; // total failed API calls per query, across all rounds
|
|
@@ -391,6 +392,7 @@ opts) {
|
|
|
391
392
|
const idleAbort = new AbortController(); // the watchdog's own kill switch
|
|
392
393
|
const signal = AbortSignal.any([opts.signal, idleAbort.signal]); // either the user or the watchdog can abort
|
|
393
394
|
const word = thinkingWord(modelCallSeq++); // a rotating "thinking" word for this call
|
|
395
|
+
const sentCount = messages.length; // how much history this request carries — the usage anchor (context.ts)
|
|
394
396
|
const startedAt = Date.now(); // for the spinner's live elapsed counter
|
|
395
397
|
let reportedTokens;
|
|
396
398
|
if (opts.progress) {
|
|
@@ -401,7 +403,9 @@ opts) {
|
|
|
401
403
|
// Reasoning is COLLAPSED by default: while the model thinks, the spinner keeps
|
|
402
404
|
// spinning (with a 💭 hint) instead of dumping the trace; we stash it for Ctrl+R.
|
|
403
405
|
let reasoningBuf = ""; // the accumulated thinking, hidden behind the spinner
|
|
404
|
-
let reasoningActive = false; // true while reasoning tokens are arriving → spinner
|
|
406
|
+
let reasoningActive = false; // true while reasoning tokens are arriving → spinner says "thinking"
|
|
407
|
+
let reasoningStartedAt = 0; // when the first reasoning token arrived (0 = no reasoning this call)
|
|
408
|
+
let reasoningEndedAt = 0; // when it stopped → "thought for Ns"
|
|
405
409
|
let reasoningShown = false; // has this trace been stored for Ctrl+R?
|
|
406
410
|
const spinner = opts.quiet || opts.subAgent
|
|
407
411
|
? null // the eval harness wants silence
|
|
@@ -414,10 +418,11 @@ opts) {
|
|
|
414
418
|
// - stall (slow but alive for 30s) → log it and keep waiting
|
|
415
419
|
// It also ticks the spinner: elapsed seconds + a live token estimate (~chars/4).
|
|
416
420
|
const watchdog = setInterval(() => {
|
|
417
|
-
// While the model is reasoning
|
|
418
|
-
//
|
|
421
|
+
// While the model is reasoning the spinner says "thinking", then "thought
|
|
422
|
+
// for Ns" — inside the parentheses, so the verb never shifts sideways.
|
|
423
|
+
const thinking = reasoningStartedAt ? { active: reasoningActive, ms: (reasoningActive ? Date.now() : reasoningEndedAt) - reasoningStartedAt } : undefined;
|
|
419
424
|
if (spinner?.spinning)
|
|
420
|
-
spinner.set(
|
|
425
|
+
spinner.set(spinnerText(word, Math.floor((Date.now() - startedAt) / 1000), !!opts.subAgent, Math.round(streamedChars / 4), thinking));
|
|
421
426
|
const quietMs = Date.now() - lastEvent; // ms since the last event
|
|
422
427
|
if (quietMs > IDLE_TIMEOUT_MS) {
|
|
423
428
|
emit("agent_watchdog_idle"); // record the cut — these should be rare
|
|
@@ -449,6 +454,7 @@ opts) {
|
|
|
449
454
|
return;
|
|
450
455
|
reasoningShown = true;
|
|
451
456
|
reasoningActive = false;
|
|
457
|
+
reasoningEndedAt = Date.now();
|
|
452
458
|
if (opts.quiet || opts.subAgent)
|
|
453
459
|
return; // no UI for silent / nested runs
|
|
454
460
|
recordReasoning(reasoningBuf); // keep it for Ctrl+R (never goes into history)
|
|
@@ -458,6 +464,7 @@ opts) {
|
|
|
458
464
|
stallWarned = false; // the stream spoke — reset the stall warning
|
|
459
465
|
if (chunk.usage) {
|
|
460
466
|
recordUsage(chunk.usage);
|
|
467
|
+
recordContextUsage(messages, sentCount, chunk.usage); // the real size of what we just sent — anchors the context count
|
|
461
468
|
if (typeof chunk.usage.completion_tokens === "number")
|
|
462
469
|
reportedTokens = chunk.usage.completion_tokens;
|
|
463
470
|
} // the final usage chunk — meter it
|
|
@@ -474,7 +481,9 @@ opts) {
|
|
|
474
481
|
if (opts.progress)
|
|
475
482
|
opts.progress.liveTokens = Math.ceil(streamedChars / 4);
|
|
476
483
|
reasoningBuf += reasoning; // stash it for Ctrl+R instead of streaming it
|
|
477
|
-
reasoningActive = true; // the spinner now
|
|
484
|
+
reasoningActive = true; // the spinner now says "thinking" (set by the watchdog)
|
|
485
|
+
if (!reasoningStartedAt)
|
|
486
|
+
reasoningStartedAt = Date.now();
|
|
478
487
|
}
|
|
479
488
|
if (delta.content) {
|
|
480
489
|
flushReasoning();
|
|
@@ -1315,14 +1324,26 @@ opts) {
|
|
|
1315
1324
|
return { reason: TerminateReason.ReviewLimit };
|
|
1316
1325
|
receiveFollowUps();
|
|
1317
1326
|
// Proactive compaction: act BEFORE the API rejects us. Waiting for the
|
|
1318
|
-
// hard limit means the failure already happened.
|
|
1319
|
-
|
|
1327
|
+
// hard limit means the failure already happened. The count is the last
|
|
1328
|
+
// response's real prompt_tokens + an estimate of what was added since, so
|
|
1329
|
+
// the threshold can sit close to the window (see compactThreshold).
|
|
1330
|
+
if (contextTokens(messages) > compactThreshold(opts.model)) {
|
|
1320
1331
|
if (compaction.count >= MAX_COMPACTIONS_PER_QUERY)
|
|
1321
1332
|
return { reason: TerminateReason.CompactionFailed, detail: `already compacted ${compaction.count}x this query — the task is too big for one session` };
|
|
1322
1333
|
const ok = await tryCompact(messages, opts, compaction); // shrink the history
|
|
1323
1334
|
if (!ok && compaction.failures >= MAX_COMPACT_FAILURES)
|
|
1324
1335
|
return { reason: TerminateReason.CompactionFailed, detail: `${compaction.failures} consecutive compaction failures` }; // the compaction circuit breaker
|
|
1325
1336
|
}
|
|
1337
|
+
// Skills: pick up any SKILL.md added or edited since the last call (no
|
|
1338
|
+
// restart), then tell the model about skills it hasn't seen yet — a
|
|
1339
|
+
// <system-reminder> user message with just the delta, as Claude Code does.
|
|
1340
|
+
// Idempotent: a retry of this same call finds the reminder already sent.
|
|
1341
|
+
// Teammates have no skill tool, so they get no listing.
|
|
1342
|
+
const skills = currentSkills();
|
|
1343
|
+
syncSkillTool(skills);
|
|
1344
|
+
const listing = opts.teammate ? null : skillListingReminder(messages, skills);
|
|
1345
|
+
if (listing)
|
|
1346
|
+
messages.push({ role: "user", content: listing });
|
|
1326
1347
|
let out;
|
|
1327
1348
|
try {
|
|
1328
1349
|
out = await streamModelCall(messages, opts); // streaming call with spinner + watchdog
|
|
@@ -1461,6 +1482,11 @@ opts) {
|
|
|
1461
1482
|
const r = await runOneCall(out.toolCalls[i++], { ...opts, autoHistory: reviewHistory(messages, opts.autoHistory) });
|
|
1462
1483
|
messages.push({ role: "tool", tool_call_id: r.id, content: r.content });
|
|
1463
1484
|
}
|
|
1485
|
+
// A launched skill's body arrives as its own user message, after ALL of
|
|
1486
|
+
// this round's tool results (the tool result itself only says "Launching
|
|
1487
|
+
// skill: <name>"). Same shape as Claude Code's skill invocation.
|
|
1488
|
+
for (const body of skillBodyMessages(out.toolCalls, messages))
|
|
1489
|
+
messages.push({ role: "user", content: body });
|
|
1464
1490
|
// Cron (Day s14): deliver any triggered scheduled jobs right after this
|
|
1465
1491
|
// round's results so the model sees them on the next round.
|
|
1466
1492
|
injectCronMessages(messages, opts);
|