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.
@@ -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 { estimateHistoryTokens, compactHistory, COMPACT_AT, MAX_COMPACTIONS_PER_QUERY, MAX_COMPACT_FAILURES } from "./context.js"; // context management
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 shows 💭
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, prefix the spinner with 💭 so "it's thinking"
418
- // reads differently from "it's waiting on the first token".
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((reasoningActive ? chalk.dim("💭 ") : "") + spinnerText(word, Math.floor((Date.now() - startedAt) / 1000), !!opts.subAgent, Math.round(streamedChars / 4)));
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 shows 💭 (set by the watchdog)
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
- if (estimateHistoryTokens(messages) > COMPACT_AT) {
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);