agent-from-zero 0.34.2 → 0.35.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/effort.js ADDED
@@ -0,0 +1,81 @@
1
+ import { CONFIG } from "./config.js";
2
+ const selections = new Map();
3
+ const keyFor = (model) => `${CONFIG.baseURL}\n${model}`;
4
+ export function effortProfile(model) {
5
+ const configured = CONFIG.effortProfiles[model];
6
+ if (configured) {
7
+ if (!Array.isArray(configured.levels) || !configured.levels.every((v) => typeof v === "string" && /^[a-z]+$/.test(v) && v !== "default")
8
+ || !["reasoning_effort", "deepseek"].includes(configured.parameter))
9
+ return undefined;
10
+ return configured;
11
+ }
12
+ let host = "";
13
+ try {
14
+ host = new URL(CONFIG.baseURL).hostname;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ if (host === "api.deepseek.com" && ["deepseek-flash", "deepseek-v4-pro"].includes(model)) {
20
+ return { levels: ["none", "low", "high", "max"], parameter: "deepseek", default: "high" };
21
+ }
22
+ if (host === "api.openai.com") {
23
+ const name = model.replace(/-\d{4}-\d{2}-\d{2}$/, "");
24
+ const levels = {
25
+ "gpt-5": ["minimal", "low", "medium", "high"],
26
+ "gpt-5-mini": ["minimal", "low", "medium", "high"],
27
+ "gpt-5-nano": ["minimal", "low", "medium", "high"],
28
+ "gpt-5.1": ["none", "low", "medium", "high"],
29
+ "gpt-5.2": ["none", "low", "medium", "high", "xhigh"],
30
+ "o3": ["low", "medium", "high"], "o3-mini": ["low", "medium", "high"], "o4-mini": ["low", "medium", "high"],
31
+ };
32
+ if (levels[name])
33
+ return { levels: levels[name], parameter: "reasoning_effort" };
34
+ }
35
+ return undefined;
36
+ }
37
+ export function selectedEffort(model) {
38
+ const selected = selections.get(keyFor(model));
39
+ return selected && effortProfile(model)?.levels.includes(selected) ? selected : "default";
40
+ }
41
+ export function effortMenu(model) {
42
+ const profile = effortProfile(model);
43
+ const current = selectedEffort(model);
44
+ const values = profile?.levels.length ? ["default", ...profile.levels] : [];
45
+ return {
46
+ header: values.length ? `effort — ${model} · current: ${current}`
47
+ : `Effort capabilities unknown or not configurable for ${model} at this endpoint. Configure effortProfiles for a custom gateway.`,
48
+ values,
49
+ labels: values.map((v) => `${v}${v === "default" ? ` (provider default${profile?.default ? `: ${profile.default}` : ""})` : v === "none" ? " (thinking off)" : ""}${v === current ? " ✓" : ""}`),
50
+ };
51
+ }
52
+ export function setEffort(model, value) {
53
+ if (value === "default") {
54
+ selections.delete(keyFor(model));
55
+ return `effort: ${model} → provider default`;
56
+ }
57
+ const menu = effortMenu(model);
58
+ if (!menu.values.includes(value))
59
+ return menu.values.length ? `Unsupported effort '${value}' for ${model}. Choose: ${menu.values.join(", ")}` : menu.header;
60
+ selections.set(keyFor(model), value);
61
+ return `effort: ${model} → ${value} (this session)`;
62
+ }
63
+ export function effortParameters(model) {
64
+ const value = selectedEffort(model);
65
+ if (value === "default")
66
+ return {};
67
+ if (effortProfile(model)?.parameter === "deepseek") {
68
+ return value === "none" ? { thinking: { type: "disabled" } }
69
+ : { thinking: { type: "enabled" }, reasoning_effort: value };
70
+ }
71
+ return { reasoning_effort: value };
72
+ }
73
+ export function retainsReasoning(model) { return effortProfile(model)?.parameter === "deepseek"; }
74
+ export function effortMessages(model, messages) {
75
+ return messages.map((message) => {
76
+ if (message.role !== "assistant")
77
+ return message;
78
+ const { reasoning_content, ...plain } = message;
79
+ return retainsReasoning(model) ? { ...plain, reasoning_content: reasoning_content ?? "" } : plain;
80
+ });
81
+ }
@@ -0,0 +1,22 @@
1
+ // Frontends enqueue; only the lead loop consumes, at a complete tool boundary.
2
+ // No terminal, provider or permission policy lives in this queue.
3
+ export class FollowUpQueue {
4
+ changed;
5
+ messages = [];
6
+ constructor(changed = () => { }) {
7
+ this.changed = changed;
8
+ }
9
+ get size() { return this.messages.length; }
10
+ get pending() { return this.messages; }
11
+ enqueue(message) {
12
+ this.messages.push(message);
13
+ this.changed();
14
+ }
15
+ drain() {
16
+ const messages = this.messages;
17
+ this.messages = [];
18
+ if (messages.length)
19
+ this.changed();
20
+ return messages;
21
+ }
22
+ }
package/dist/form.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import chalk from "chalk"; // rendering only
2
- // The rows are a flat list: every option of every question, then one submit row.
3
- // rowCount = total options + 1. The cursor walks this flat list.
2
+ // The cursor walks a flat list of options. Multiple questions add a submit row;
3
+ // with one question, explicitly choosing an option submits it immediately.
4
4
  export function formRowCount(questions) {
5
- return questions.reduce((n, q) => n + q.options.length, 0) + 1; // +1 for "Submit"
5
+ return questions.reduce((n, q) => n + q.options.length, 0) + (questions.length > 1 ? 1 : 0);
6
6
  }
7
7
  // Map a flat cursor to either a question/option, or the submit row.
8
8
  export function rowAt(questions, cursor) {
@@ -43,7 +43,7 @@ export function reduceForm(questions, state, action) {
43
43
  }
44
44
  const selections = state.selections.slice();
45
45
  selections[row.q] = row.opt; // set this question's answer (single-select)
46
- return { state: { ...state, selections } };
46
+ return { state: { ...state, selections }, ...(questions.length === 1 ? { done: true } : {}) };
47
47
  }
48
48
  // Turn a completed form into question→answer pairs for the model.
49
49
  export function collectAnswers(questions, state) {
@@ -71,8 +71,10 @@ export function renderForm(questions, state) {
71
71
  const onSubmit = state.cursor === flat; // the submit row is last
72
72
  const allAnswered = state.selections.every((s) => s >= 0);
73
73
  const submitText = allAnswered ? "▶ Submit answers" : "▶ Submit answers (answer every question first)";
74
- lines.push(onSubmit ? chalk.green.bold(`❯ ${submitText}`) : chalk.dim(` ${submitText}`));
75
- lines.push("");
74
+ if (questions.length > 1) {
75
+ lines.push(onSubmit ? chalk.green.bold(`❯ ${submitText}`) : chalk.dim(` ${submitText}`));
76
+ lines.push("");
77
+ }
76
78
  lines.push(FORM_HINT);
77
79
  return lines.join("\n");
78
80
  }
package/dist/images.js ADDED
@@ -0,0 +1,43 @@
1
+ import { systemClipboard } from "./clipboard.js";
2
+ export const MAX_IMAGES = 4;
3
+ export const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
4
+ export const imageLabel = (id) => `[Image #${id}]`;
5
+ export function imageFromBytes(data, id) {
6
+ if (data.length > MAX_IMAGE_BYTES)
7
+ throw new Error("Image exceeds 5 MB. Copy a smaller image.");
8
+ let mime;
9
+ if (data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
10
+ mime = "image/png";
11
+ else if (data[0] === 255 && data[1] === 216 && data[2] === 255)
12
+ mime = "image/jpeg";
13
+ else if (["GIF87a", "GIF89a"].includes(data.subarray(0, 6).toString()))
14
+ mime = "image/gif";
15
+ else if (data.subarray(0, 4).toString() === "RIFF" && data.subarray(8, 12).toString() === "WEBP")
16
+ mime = "image/webp";
17
+ else
18
+ throw new Error("Clipboard does not contain a supported PNG, JPEG, GIF or WebP image.");
19
+ return { id, dataUrl: `data:${mime};base64,${data.toString("base64")}` };
20
+ }
21
+ // UI and attachment handling depend on this interface, never on an OS API.
22
+ export async function readClipboard(id, source = systemClipboard()) {
23
+ const content = await source.read();
24
+ return content.image ? { image: imageFromBytes(content.image, id) } : { text: content.text ?? "" };
25
+ }
26
+ // The numbered text labels stay next to their image parts so multiple images
27
+ // can be referenced unambiguously. Plain text remains a plain text message.
28
+ export function userContent(text, images) {
29
+ if (!images.length)
30
+ return text;
31
+ return [
32
+ { type: "text", text },
33
+ ...images.flatMap((image) => [
34
+ { type: "text", text: imageLabel(image.id) },
35
+ { type: "image_url", image_url: { url: image.dataUrl } },
36
+ ]),
37
+ ];
38
+ }
39
+ // Deleting a label removes its attachment too; raw image bytes never enter
40
+ // the editable buffer, terminal transcript, title generation or auto judge.
41
+ export function imagesInInput(text, images) {
42
+ return images.filter((image) => text.includes(imageLabel(image.id)));
43
+ }
@@ -0,0 +1,62 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import wrapAnsi from "wrap-ansi";
3
+ import { formatElapsed } from "../ui.js";
4
+ function plain(text) {
5
+ return stripVTControlCharacters(text).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
6
+ }
7
+ function oneLine(text) { return plain(text).replace(/\s+/g, " ").trim().slice(0, 180); }
8
+ function title(call) {
9
+ const labels = {
10
+ read_file: "Read", search: "Search", run_bash: "Run", run_bash_background: "Start background task",
11
+ bash_output: "Check background task", write_file: "Write", edit_file: "Edit",
12
+ };
13
+ let target = "";
14
+ try {
15
+ const args = JSON.parse(call.args ?? "{}");
16
+ target = [call.name === "search" ? args.pattern : undefined, args.path, args.command, args.task_id].find((v) => typeof v === "string") ?? "";
17
+ }
18
+ catch { /* malformed input is shown in full in the details */ }
19
+ return oneLine(`${labels[call.name ?? ""] ?? call.name ?? "Tool"}${target ? ` (${target})` : ""}`);
20
+ }
21
+ // Consecutive calls of the same type share one card. Polling 100 times still
22
+ // takes two lines; only the most recent groups occupy the transient viewport.
23
+ export function summarizeActivity(calls, now = Date.now(), limit = 2) {
24
+ const groups = [];
25
+ for (const call of calls) {
26
+ const previous = groups.at(-1);
27
+ if (previous && previous[0].name === call.name)
28
+ previous.push(call);
29
+ else
30
+ groups.push([call]);
31
+ }
32
+ return groups.slice(-limit).map((group) => {
33
+ const last = group.at(-1);
34
+ const running = group.some((c) => c.endedAt === undefined);
35
+ const failed = group.some((c) => /^\[(?:error|permission|hook)\]/.test(c.result ?? ""));
36
+ const completed = group.filter((c) => c.endedAt !== undefined).length;
37
+ const duration = formatElapsed((running ? now : Math.max(...group.map((c) => c.endedAt))) - group[0].startedAt);
38
+ let result = last.result === undefined ? "In progress" : "Completed";
39
+ if (last.result !== undefined) {
40
+ if (/^\[(?:error|permission|hook)\]/.test(last.result))
41
+ result = oneLine(last.result);
42
+ else if (last.name === "read_file")
43
+ result = `Read ${last.result === "(empty file)" ? 0 : last.result.split("\n").length} lines`;
44
+ else if (last.name === "search")
45
+ result = last.result.startsWith("No matches.") ? "No matches" : `Found ${last.result.split("\n").filter((line) => /:\d+:/.test(line)).length} matches`;
46
+ else
47
+ result = oneLine(last.result.split("\n")[0]) || "Completed (no output)";
48
+ }
49
+ const count = group.length > 1 ? `${completed}/${group.length} calls · ` : "";
50
+ return { title: title(last), detail: `${count}${result} · ${duration}${failed ? " · needs attention" : ""}`, running, failed };
51
+ });
52
+ }
53
+ // Page from the end so opening a trace shows the latest action. Wrapping before
54
+ // slicing bounds physical terminal rows, including CJK and long JSON strings.
55
+ export function detailPage(text, page, rows, columns) {
56
+ const height = Math.max(1, rows);
57
+ const lines = wrapAnsi(plain(text), Math.max(1, columns), { hard: true, trim: false }).split("\n");
58
+ const pages = Math.max(1, Math.ceil(lines.length / height));
59
+ const selected = Math.max(0, Math.min(page, pages - 1));
60
+ const end = lines.length - selected * height;
61
+ return { text: lines.slice(Math.max(0, end - height), end).join("\n"), page: selected, pages };
62
+ }
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { formatElapsed, formatTokens } from "../ui.js";
4
+ // Use stable IDs so a newly spawned worker cannot move keyboard focus.
5
+ export function moveAgentFocus(ids, current, delta) {
6
+ const index = current === null ? -1 : ids.indexOf(current);
7
+ const next = index + delta;
8
+ return next < 0 ? null : ids[Math.min(next, ids.length - 1)] ?? null;
9
+ }
10
+ export function AgentList({ agents, focus, viewing, mainBusy, maxRows = 5 }) {
11
+ if (!agents.length)
12
+ return null;
13
+ const rows = [{ id: "main", name: "main", description: "", right: mainBusy ? "working" : "idle", failed: false }, ...agents.map((a) => ({
14
+ id: a.id, name: a.name, description: a.description, failed: a.status === "failed",
15
+ right: `${a.status === "running" ? a.activity : a.status} · ${formatElapsed(a.elapsedMs)} · ↓ ${a.estimated ? "~" : ""}${formatTokens(a.tokens)} tokens`,
16
+ }))];
17
+ const selected = Math.max(0, rows.findIndex((r) => r.id === (focus ?? viewing ?? "main")));
18
+ const start = Math.max(0, Math.min(selected - maxRows + 1, rows.length - maxRows));
19
+ return _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["agents \u00B7 Tab/\u2190 select \u00B7 \u2191\u2193 switch \u00B7 Enter view \u00B7 Esc back", rows.length > maxRows ? ` · ${start + 1}–${Math.min(rows.length, start + maxRows)}/${rows.length}` : ""] }), rows.slice(start, start + maxRows).map((row) => {
20
+ const highlighted = focus === row.id;
21
+ const active = (viewing ?? "main") === row.id;
22
+ return _jsxs(Box, { children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, children: _jsxs(Text, { wrap: "truncate-end", color: highlighted ? "cyan" : row.failed ? "yellow" : undefined, bold: active || highlighted, dimColor: !active && !highlighted, children: [highlighted ? "❯" : active ? "●" : "○", " ", row.name, row.description ? ` · ${row.description}` : ""] }) }), _jsx(Box, { flexShrink: 0, marginLeft: 1, children: _jsx(Text, { dimColor: true, color: row.failed ? "yellow" : undefined, children: row.right }) })] }, row.id);
23
+ })] });
24
+ }