@livx.cc/agentx 0.99.19 → 0.99.21
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 +1 -0
- package/dist/{Agent-Cn9BxS8h.d.ts → Agent-CM7HL95r.d.ts} +8 -3
- package/dist/cli.d.ts +7 -4
- package/dist/cli.js +162 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DopVRDLx.d.ts → mcp-2wyAAfPQ.d.ts} +1 -1
- package/dist/mcp.client.d.ts +2 -2
- package/dist/{tools-DKf3hN4M.d.ts → tools-ClgFZjUr.d.ts} +5 -0
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ agentx --resume <id> "…" # resume a specific session
|
|
|
114
114
|
- **Project instructions** — `./AGENTS.md` (or `CLAUDE.md`) auto-loads into every run; `/init` scaffolds one.
|
|
115
115
|
- **Any provider** — set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`; choose with `-m provider/model`. Env files load with precedence: CWD `.env` (bun) → install-dir `.env` → `~/.agent/.env` (user-wide). **Bodify secrets**: set `BODIFY_API_KEY` + `BODIFY_APP_ID` (e.g. in `~/.agent/.env`) to pull provider keys from a Bodify app at startup — no local key management needed.
|
|
116
116
|
- **@-file mentions & headless JSON** — reference files inline in a prompt with `@path` (e.g. `explain @src/Agent.ts`; `~/` expands to the home directory; quote paths with spaces as `@"…"` — drag-dropped files, e.g. macOS screenshots, quote themselves automatically); script with `-p --output-format json` to get one machine-readable result object on stdout (activity stays on stderr).
|
|
117
|
+
- **Raise, don't guess** (`AskUserQuestion` + `finishReason: 'needs_input'`) — when a decision is genuinely the user's, the agent asks. Interactively that's an arrow-select prompt. **Headless** (`-p`, piped, no TTY) there's nobody to prompt, so instead of blocking on a prompt no one can see — or, worse, "proceeding with best judgment" — the run **stops and hands the question to its caller**: exit code `2`, a yellow `? needs input` footer, and `{finishReason:'needs_input', question:{…}, sessionId}` in `--output-format json`. The transcript is preserved, so the caller answers and continues (`--resume <id> -p "<answer>"`) instead of re-running. A delegated agent silently deciding something that was never its to decide is the failure this closes.
|
|
117
118
|
- **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
|
|
118
119
|
- **Duplex mode** — `agentx --duplex` runs the full standard REPL (slash commands, sessions, postures, rewind, MCP) with the three-tier engine driving turns: a fast voice model (`--voice-model`, default `groq/openai/gpt-oss-120b`) answers every line instantly and delegates real work to background workers built with the same wiring as a normal run (fs mode, permissions, MCP); worker activity shows as dim chrome and results are re-voiced when ready. Switch any tier live with `/model` (opens a reflex/act/think picker), or the `/voice-model` · `/think-model` shortcuts. `/tasks` lists background tasks, inspects a task's live output tail, and cancels a running one from a picker (Esc mid-turn cancels the foreground turn; Esc again at the idle prompt cancels running workers).
|
|
119
120
|
- **MCP servers** — declare `mcpServers: { name: { command, args } | { url } }` in config and they're auto-mounted at startup (in parallel, with an optional `mountTimeoutMs` deadline so one slow/dead server never blocks the rest): the client does the JSON-RPC handshake (stdio or HTTP) + `tools/list`, and the discovered tools appear as `mcp__<name>__<tool>` in `/tools` (inspect with `/mcp`). A bad server is logged and skipped, never blocking the agent. For large tool sets, **deferred mode** (`makeMcpToolSearch` / `mountMcpDeferred`) exposes just two bounded tools (`ToolSearch` + `McpCall`) instead of N defs — dodging the provider tool-cap and improving selection accuracy; the CLI applies this automatically past 12 mounted tools (a 42-tool server was costing ~80k tok/turn in schema alone), and permission rules written against the real `mcp__<name>__<tool>` names still match through `McpCall`. **`mountMcpCatalog`** goes further: a cached, hash-keyed catalog + lazy connect means a turn that uses no MCP tool opens **zero** connections, and one that uses a tool connects exactly that server — latency scales with tools-used, not servers-configured. A down server is **negative-cached** (`failureCooldownMs`) so it never re-floors a later turn at the deadline. For zero turn-path latency even on a cold process, call **`warmMcpCatalog`** at boot + on a timer (off-turn discovery) and mount with **`{ discover: 'cache-only' }`** — the turn then never synchronously connects: it serves the warmed catalog and discovers any miss in the background.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
2
|
-
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, e as MessageContent } from './tools-
|
|
2
|
+
import { M as Message, H as HostBridge, A as AgentTool, C as ChatLike, e as MessageContent, U as UserQuestion } from './tools-ClgFZjUr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Hooks — deterministic interception points around tool execution, run by the
|
|
@@ -180,9 +180,14 @@ declare function reasoningToChatFragment(model: string, effort?: ReasoningEffort
|
|
|
180
180
|
interface RunResult {
|
|
181
181
|
text: string;
|
|
182
182
|
steps: number;
|
|
183
|
-
/** Why the loop ended. The middle group are automatic kill-switches (budget/abuse guards).
|
|
184
|
-
|
|
183
|
+
/** Why the loop ended. The middle group are automatic kill-switches (budget/abuse guards).
|
|
184
|
+
* `needs_input` is NOT a failure: the agent hit a decision that was the user's to make with no
|
|
185
|
+
* human reachable, so it stopped and raised the question (see `question`) instead of guessing.
|
|
186
|
+
* Answer it and resume the session to continue. */
|
|
187
|
+
finishReason: 'stop' | 'max_steps' | 'budget' | 'timeout' | 'loop' | 'max_tool_calls' | 'aborted' | 'error' | 'needs_input';
|
|
185
188
|
messages: Message[];
|
|
189
|
+
/** The parked question — present iff `finishReason === 'needs_input'`. */
|
|
190
|
+
question?: UserQuestion;
|
|
186
191
|
/** Accumulated token usage across all turns (non-stream path). With prompt caching,
|
|
187
192
|
* promptTokens includes cached reads/writes; the cache splits ride along for exact pricing. */
|
|
188
193
|
usage?: {
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { H as Hooks, h as RunResult, R as ReasoningEffort, A as Agent } from './Agent-
|
|
2
|
+
import { H as Hooks, h as RunResult, R as ReasoningEffort, A as Agent } from './Agent-CM7HL95r.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
|
-
import { M as Message, H as HostBridge, c as ContentPart, e as MessageContent } from './tools-
|
|
4
|
+
import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, e as MessageContent } from './tools-ClgFZjUr.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* On-disk session store for the CLI: each conversation is one JSON file at
|
|
@@ -150,6 +150,7 @@ declare function makeHost(format?: 'text' | 'json' | 'stream-json', opts?: {
|
|
|
150
150
|
stream?: boolean;
|
|
151
151
|
}): HostBridge & {
|
|
152
152
|
flushText: () => void;
|
|
153
|
+
takeToolCount: () => number;
|
|
153
154
|
};
|
|
154
155
|
/** Hooks that render tool activity to stderr: a preToolUse header, and for edits a colorized diff.
|
|
155
156
|
* `opts.background` marks the agent as a BACKGROUND one (duplex workers): its chrome lands async on
|
|
@@ -235,7 +236,8 @@ interface PermMode {
|
|
|
235
236
|
* Default permission posture — CC-inspired: **ask when a human can answer, allow when unattended.**
|
|
236
237
|
* Pure + exported so the decision is unit-testable without a TTY. Precedence:
|
|
237
238
|
* --yes → allow (explicit trust, no prompts)
|
|
238
|
-
* --ask → ask (explicit gate
|
|
239
|
+
* --ask → ask (explicit gate; parseArgs rejects it up front when no terminal is attached,
|
|
240
|
+
* so this branch always has a human behind it)
|
|
239
241
|
* interactive (TTY & not json) → ask (the safe default)
|
|
240
242
|
* else (headless/piped) → allow, with a one-line notice so it's never silent.
|
|
241
243
|
*/
|
|
@@ -270,8 +272,9 @@ declare function expandMentions(fs: IFilesystem, line: string): Promise<{
|
|
|
270
272
|
/** The headless `--output-format json` result object for a turn. */
|
|
271
273
|
declare function jsonResult(res: RunResult, session: SessionData): {
|
|
272
274
|
error?: any;
|
|
275
|
+
question?: UserQuestion | undefined;
|
|
273
276
|
ok: boolean;
|
|
274
|
-
finishReason: "error" | "budget" | "stop" | "max_steps" | "timeout" | "loop" | "max_tool_calls" | "aborted";
|
|
277
|
+
finishReason: "error" | "budget" | "stop" | "max_steps" | "timeout" | "loop" | "max_tool_calls" | "aborted" | "needs_input";
|
|
275
278
|
text: string;
|
|
276
279
|
steps: number;
|
|
277
280
|
tools: number;
|
package/dist/cli.js
CHANGED
|
@@ -2152,7 +2152,7 @@ ${out.replace(/\n+$/, "") || "(no output yet)"}`;
|
|
|
2152
2152
|
// src/host.ts
|
|
2153
2153
|
var askUserQuestionTool = {
|
|
2154
2154
|
name: "AskUserQuestion",
|
|
2155
|
-
description:
|
|
2155
|
+
description: 'Ask the user a multiple-choice question when a decision is genuinely theirs to make: an ambiguous or under-specified requirement, a fork you cannot resolve from context, a premise of the task that the evidence contradicts, or a risky/irreversible next step. Prefer this over guessing, "proceeding with best judgment", or quietly widening scope to make an unclear brief fit \u2014 a wrong silent decision is far more costly than a question. Returns their selection if a human is reachable; otherwise the run pauses and the question is handed to whoever delegated the task (you do not need to know which \u2014 just ask).',
|
|
2156
2156
|
parameters: {
|
|
2157
2157
|
type: "object",
|
|
2158
2158
|
required: ["question", "options"],
|
|
@@ -2168,8 +2168,8 @@ var askUserQuestionTool = {
|
|
|
2168
2168
|
},
|
|
2169
2169
|
async run(args, ctx) {
|
|
2170
2170
|
if (!ctx.host?.ask) {
|
|
2171
|
-
|
|
2172
|
-
return
|
|
2171
|
+
ctx.needsInput = args;
|
|
2172
|
+
return "PAUSED \u2014 no interactive user is reachable from this run, so your question has been raised to the caller that delegated this task. The run stops here; do not guess and do not continue. Their answer will arrive as the next user message when the run resumes.";
|
|
2173
2173
|
}
|
|
2174
2174
|
const answer = ctx.host.ask(args);
|
|
2175
2175
|
return ctx.parkHuman ? ctx.parkHuman(answer) : answer;
|
|
@@ -3617,6 +3617,7 @@ var Agent = class _Agent {
|
|
|
3617
3617
|
let usageEstimated = false;
|
|
3618
3618
|
const start = Date.now();
|
|
3619
3619
|
this.parkedMs = 0;
|
|
3620
|
+
this.ctx.needsInput = void 0;
|
|
3620
3621
|
let toolCallsTotal = 0;
|
|
3621
3622
|
let lastFp = "";
|
|
3622
3623
|
let repeats = 0;
|
|
@@ -3750,6 +3751,17 @@ var Agent = class _Agent {
|
|
|
3750
3751
|
}
|
|
3751
3752
|
this.transcript.push({ role: "tool", tool_call_id: tc.id, name: tc.function.name, content });
|
|
3752
3753
|
}
|
|
3754
|
+
if (this.ctx.needsInput) {
|
|
3755
|
+
const question = this.ctx.needsInput;
|
|
3756
|
+
this.ctx.needsInput = void 0;
|
|
3757
|
+
log4.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
3758
|
+
await this.ctx.jobs?.drain();
|
|
3759
|
+
const text = [
|
|
3760
|
+
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
3761
|
+
...question.options.map((o2) => ` \u2022 ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)
|
|
3762
|
+
].join("\n");
|
|
3763
|
+
return { text, steps, finishReason: "needs_input", messages: this.transcript, usage, usageEstimated, question };
|
|
3764
|
+
}
|
|
3753
3765
|
this.drainInjections();
|
|
3754
3766
|
}
|
|
3755
3767
|
}
|
|
@@ -10160,19 +10172,112 @@ function renderMdLine(line, inFence, p, cols = 80, fenceLang) {
|
|
|
10160
10172
|
function renderMdBlock(text, p, cols = 80) {
|
|
10161
10173
|
let inFence = false, fenceLang;
|
|
10162
10174
|
const out = [];
|
|
10175
|
+
let table = [];
|
|
10176
|
+
const flush = () => {
|
|
10177
|
+
if (table.length) {
|
|
10178
|
+
out.push(...renderTableLines(table, p, cols));
|
|
10179
|
+
table = [];
|
|
10180
|
+
}
|
|
10181
|
+
};
|
|
10163
10182
|
for (const line of text.split("\n")) {
|
|
10183
|
+
if (!inFence && isTableRow(line)) {
|
|
10184
|
+
table.push(line);
|
|
10185
|
+
continue;
|
|
10186
|
+
}
|
|
10187
|
+
flush();
|
|
10164
10188
|
const r = renderMdLine(line, inFence, p, cols, fenceLang);
|
|
10165
10189
|
inFence = r.inFence;
|
|
10166
10190
|
fenceLang = r.fenceLang;
|
|
10167
10191
|
out.push(r.inFence || /^\s*```/.test(line) ? r.out : wrapAnsi(r.out, cols, hangIndent(line)));
|
|
10168
10192
|
}
|
|
10193
|
+
flush();
|
|
10169
10194
|
return out.join("\n");
|
|
10170
10195
|
}
|
|
10196
|
+
var T_PAD = " ";
|
|
10197
|
+
var T_GAP = 3;
|
|
10198
|
+
var SGR_CLOSES = {
|
|
10199
|
+
"0": /./,
|
|
10200
|
+
"22": /^[12]$/,
|
|
10201
|
+
"23": /^3$/,
|
|
10202
|
+
"24": /^4$/,
|
|
10203
|
+
"27": /^7$/,
|
|
10204
|
+
"29": /^9$/,
|
|
10205
|
+
"39": /^(3[0-7]|9[0-7])$/,
|
|
10206
|
+
"49": /^(4[0-7]|10[0-7])$/
|
|
10207
|
+
};
|
|
10208
|
+
function wrapCell(styled, w) {
|
|
10209
|
+
const segs = wrapAnsi(styled, w).split("\r\n");
|
|
10210
|
+
if (segs.length < 2 || !styled.includes("\x1B")) return segs;
|
|
10211
|
+
const tok = new RegExp(ANSI_TOKEN.source, "g");
|
|
10212
|
+
let open = [];
|
|
10213
|
+
return segs.map((seg) => {
|
|
10214
|
+
const prefix = open.join("");
|
|
10215
|
+
for (const m of seg.match(tok) ?? []) {
|
|
10216
|
+
const code = /^\x1b\[([0-9;]*)m$/.exec(m)?.[1];
|
|
10217
|
+
if (code === void 0) continue;
|
|
10218
|
+
const closes = SGR_CLOSES[code || "0"];
|
|
10219
|
+
if (closes) open = open.filter((o) => !closes.test(/^\x1b\[([0-9;]*)m$/.exec(o)[1]));
|
|
10220
|
+
else open.push(m);
|
|
10221
|
+
}
|
|
10222
|
+
return prefix + seg + (open.length ? "\x1B[0m" : "");
|
|
10223
|
+
});
|
|
10224
|
+
}
|
|
10225
|
+
var T_MINCOL = 2;
|
|
10226
|
+
function fitWidths(natural, avail) {
|
|
10227
|
+
const total = natural.reduce((a, b) => a + b, 0);
|
|
10228
|
+
if (total <= avail || !natural.length) return natural;
|
|
10229
|
+
const room = Math.max(T_MINCOL * natural.length, avail);
|
|
10230
|
+
let cap = 1;
|
|
10231
|
+
for (let c = 1; c <= Math.max(...natural); c++) {
|
|
10232
|
+
if (natural.reduce((a, w2) => a + Math.min(w2, c), 0) <= room) cap = c;
|
|
10233
|
+
else break;
|
|
10234
|
+
}
|
|
10235
|
+
const w = natural.map((n) => Math.min(n, cap));
|
|
10236
|
+
let left = room - w.reduce((a, b) => a + b, 0);
|
|
10237
|
+
for (let i = 0; i < w.length && left > 0; i++) {
|
|
10238
|
+
const take = Math.min(left, natural[i] - w[i]);
|
|
10239
|
+
w[i] += take;
|
|
10240
|
+
left -= take;
|
|
10241
|
+
}
|
|
10242
|
+
return w;
|
|
10243
|
+
}
|
|
10244
|
+
function renderTableLines(rows, p, cols = 80) {
|
|
10245
|
+
const sepIdx = rows.findIndex((r, i) => i > 0 && r.includes("-") && /^\s*\|[\s:|-]+\|\s*$/.test(r));
|
|
10246
|
+
if (sepIdx < 0) return rows.map((r) => renderMdLine(r, false, p, cols).out);
|
|
10247
|
+
const cells = (r) => r.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
|
|
10248
|
+
const data = rows.filter((_, i) => i !== sepIdx).map(cells);
|
|
10249
|
+
const ncol = Math.max(...data.map((r) => r.length));
|
|
10250
|
+
const natural = Array.from({ length: ncol }, (_, i) => Math.max(...data.map((r) => displayText(r[i] ?? "").length)));
|
|
10251
|
+
const widths = fitWidths(natural, cols - T_PAD.length - T_GAP * (ncol - 1));
|
|
10252
|
+
const grid = data.map((row, ri) => widths.map((w, ci) => {
|
|
10253
|
+
const c = row[ci] ?? "";
|
|
10254
|
+
const styled = ri === 0 ? p.bold(displayText(vis(c))) : mdInline(vis(c), p);
|
|
10255
|
+
return wrapCell(styled, w);
|
|
10256
|
+
}));
|
|
10257
|
+
const airy = grid.slice(1).some((row) => row.some((c) => c.length > 1));
|
|
10258
|
+
const out = [];
|
|
10259
|
+
grid.forEach((col, ri) => {
|
|
10260
|
+
const header = ri === 0;
|
|
10261
|
+
if (airy && ri > 1) out.push("");
|
|
10262
|
+
const height = Math.max(...col.map((c) => c.length));
|
|
10263
|
+
for (let li = 0; li < height; li++) {
|
|
10264
|
+
out.push((T_PAD + widths.map((w, ci) => {
|
|
10265
|
+
const seg = col[ci][li] ?? "";
|
|
10266
|
+
return seg + " ".repeat(Math.max(0, w - visWidth(seg)));
|
|
10267
|
+
}).join(" ".repeat(T_GAP))).replace(/\s+$/, ""));
|
|
10268
|
+
}
|
|
10269
|
+
if (header) out.push(T_PAD + p.dim("\u2500".repeat(widths.reduce((a, b) => a + b, 0) + T_GAP * (ncol - 1))));
|
|
10270
|
+
});
|
|
10271
|
+
return out;
|
|
10272
|
+
}
|
|
10171
10273
|
function isTableRow(line) {
|
|
10172
10274
|
return /^\s*\|.*\|\s*$/.test(line);
|
|
10173
10275
|
}
|
|
10174
10276
|
var ANSI_TOKEN = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07\x1b]*(?:\x07|\x1b\\)/;
|
|
10175
10277
|
var WRAP_TOKENS = new RegExp(`${ANSI_TOKEN.source}|[ \\t]+|[^\\s\\x1b]+`, "g");
|
|
10278
|
+
function visWidth(s) {
|
|
10279
|
+
return s.replace(new RegExp(ANSI_TOKEN.source, "g"), "").length;
|
|
10280
|
+
}
|
|
10176
10281
|
function wrapAnsi(s, cols, indent = "") {
|
|
10177
10282
|
if (cols <= 1 || s.replace(new RegExp(ANSI_TOKEN.source, "g"), "").length <= cols) return s;
|
|
10178
10283
|
if (indent.length >= cols) indent = "";
|
|
@@ -10239,29 +10344,11 @@ var MarkdownStream = class {
|
|
|
10239
10344
|
this.cols = Math.max(1, cols);
|
|
10240
10345
|
if (this.buf) this.lineRows = Math.max(1, Math.ceil(this.buf.length / this.cols));
|
|
10241
10346
|
}
|
|
10242
|
-
/** Render the held table block as aligned columns and reset the buffer.
|
|
10243
|
-
* separator row it isn't a real table — emit the rows verbatim (rendered as normal lines) instead. */
|
|
10347
|
+
/** Render the held table block as aligned columns (shared with renderMdBlock) and reset the buffer. */
|
|
10244
10348
|
flushTable() {
|
|
10245
10349
|
const rows = this.tableBuf;
|
|
10246
10350
|
this.tableBuf = [];
|
|
10247
|
-
|
|
10248
|
-
if (sepIdx < 0) return rows.map((r) => renderMdLine(r, false, this.p, this.cols).out + "\r\n").join("");
|
|
10249
|
-
const cells = (r) => r.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
|
|
10250
|
-
const data = rows.filter((_, i) => i !== sepIdx).map(cells);
|
|
10251
|
-
const ncol = Math.max(...data.map((r) => r.length));
|
|
10252
|
-
const widths = Array.from({ length: ncol }, (_, i) => Math.max(...data.map((r) => displayText(r[i] ?? "").length)));
|
|
10253
|
-
const out = [];
|
|
10254
|
-
data.forEach((row, ri) => {
|
|
10255
|
-
const header = ri === 0;
|
|
10256
|
-
const line = " " + widths.map((w, ci) => {
|
|
10257
|
-
const c = row[ci] ?? "";
|
|
10258
|
-
const styled = header ? this.p.bold(displayText(vis(c))) : mdInline(vis(c), this.p);
|
|
10259
|
-
return styled + " ".repeat(Math.max(0, w - displayText(c).length));
|
|
10260
|
-
}).join(" ").replace(/\s+$/, "");
|
|
10261
|
-
out.push(line);
|
|
10262
|
-
if (header) out.push(" " + this.p.dim("\u2500".repeat(widths.reduce((a, b) => a + b, 0) + 3 * (ncol - 1))));
|
|
10263
|
-
});
|
|
10264
|
-
return out.map((l) => l + "\r\n").join("");
|
|
10351
|
+
return renderTableLines(rows, this.p, this.cols).map((l) => l + "\r\n").join("");
|
|
10265
10352
|
}
|
|
10266
10353
|
feed(text) {
|
|
10267
10354
|
this.buf += text;
|
|
@@ -11995,6 +12082,8 @@ function parseArgs(argv) {
|
|
|
11995
12082
|
if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
|
|
11996
12083
|
if (a.voiceModel && !a.duplex) throw new Error("--voice-model only applies with --duplex");
|
|
11997
12084
|
if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
|
|
12085
|
+
if (!canPrompt && (a.ask || a.plan))
|
|
12086
|
+
throw new Error(`${a.ask ? "--ask" : "--plan"} needs an interactive terminal to approve on \u2014 none is attached (stdin/stderr are not a TTY). For an unattended run use --yes, or pre-authorize with rules in .agent/permissions.json.`);
|
|
11998
12087
|
return a;
|
|
11999
12088
|
}
|
|
12000
12089
|
var HELP = `agentx \u2014 agent runtime CLI
|
|
@@ -12022,7 +12111,8 @@ Flags:
|
|
|
12022
12111
|
--harden OS-sandbox the real shell (sandbox-exec/bwrap): writes confined to cwd+tmp,
|
|
12023
12112
|
outbound network blocked; --harden-net keeps network allowed
|
|
12024
12113
|
--plan plan mode: edits blocked until you approve a plan
|
|
12025
|
-
--ask confirm each mutating tool (bash/Shell/Write/Edit/\u2026)
|
|
12114
|
+
--ask confirm each mutating tool (bash/Shell/Write/Edit/\u2026) \u2014 needs a terminal to
|
|
12115
|
+
approve on; rejected up front when detached (use --yes / permissions rules)
|
|
12026
12116
|
--yes, -y auto-approve mutating tools (no prompts) \u2014 for trusted/unattended runs
|
|
12027
12117
|
--update check for updates and install if available (no keys needed)
|
|
12028
12118
|
--no-update-check skip the automatic update check on startup
|
|
@@ -12054,6 +12144,10 @@ Flags:
|
|
|
12054
12144
|
|
|
12055
12145
|
Prompts may reference files with @path (e.g. "explain @src/Agent.ts") \u2014 they're inlined.
|
|
12056
12146
|
|
|
12147
|
+
Exit codes (-p): 0 = done \xB7 1 = failed \xB7 2 = needs input \u2014 the agent hit a decision that was yours
|
|
12148
|
+
to make and paused rather than guess. The question is on stderr (and in --output-format json under
|
|
12149
|
+
"question"); answer it with: agentx --resume <sessionId> -p "<your answer>".
|
|
12150
|
+
|
|
12057
12151
|
Providers: set any of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY.
|
|
12058
12152
|
Env files: .env (CWD, bun auto-loads) > install-dir .env > ~/.agent/.env (user-wide).
|
|
12059
12153
|
Bodify secrets: set BODIFY_API_KEY + BODIFY_APP_ID (in ~/.agent/.env) to pull keys from a Bodify app.
|
|
@@ -12158,8 +12252,17 @@ function makeHost(format = "text", opts) {
|
|
|
12158
12252
|
}
|
|
12159
12253
|
};
|
|
12160
12254
|
const activitySeen = /* @__PURE__ */ new Set();
|
|
12255
|
+
const turnActivityIds = /* @__PURE__ */ new Set();
|
|
12256
|
+
let anonActivityCount = 0;
|
|
12161
12257
|
return {
|
|
12162
12258
|
flushText,
|
|
12259
|
+
/** Read + reset this turn's self-executing-backend tool tally (see turnActivityIds). */
|
|
12260
|
+
takeToolCount: () => {
|
|
12261
|
+
const n = turnActivityIds.size + anonActivityCount;
|
|
12262
|
+
turnActivityIds.clear();
|
|
12263
|
+
anonActivityCount = 0;
|
|
12264
|
+
return n;
|
|
12265
|
+
},
|
|
12163
12266
|
notify(e) {
|
|
12164
12267
|
if (e.kind === "turn_start") {
|
|
12165
12268
|
activitySeen.clear();
|
|
@@ -12192,6 +12295,8 @@ function makeHost(format = "text", opts) {
|
|
|
12192
12295
|
}
|
|
12193
12296
|
if (e.kind === "tool_activity") {
|
|
12194
12297
|
const a = e;
|
|
12298
|
+
if (a.id) turnActivityIds.add(a.id);
|
|
12299
|
+
else if (a.status === "running") anonActivityCount++;
|
|
12195
12300
|
flushLookupGroup();
|
|
12196
12301
|
closeReasonLine();
|
|
12197
12302
|
flushText();
|
|
@@ -12244,21 +12349,28 @@ function makeHost(format = "text", opts) {
|
|
|
12244
12349
|
io.close();
|
|
12245
12350
|
}
|
|
12246
12351
|
},
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
const
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12352
|
+
// Only offer `ask` when a human can actually SEE the prompt. Headless/piped (`-p`, redirected,
|
|
12353
|
+
// no TTY) → omit it: AskUserQuestion then parks the question and ends the run with
|
|
12354
|
+
// `needs_input`, handing it to the caller. Prompting into a void instead blocks forever —
|
|
12355
|
+
// parkHuman excludes the wait from --timeout, so only an external timeout ever reaps it, and
|
|
12356
|
+
// the whole turn is lost. A question nobody can answer must travel UP, not stall.
|
|
12357
|
+
...canPrompt ? {
|
|
12358
|
+
async ask(q2) {
|
|
12359
|
+
const title = `? ${q2.header ? "[" + q2.header + "] " : ""}${q2.question}`;
|
|
12360
|
+
const v = await selectMenu(process.stderr, { title, items: q2.options.map((o) => ({ label: o.label, value: o.label, desc: o.description })) });
|
|
12361
|
+
if (v !== null) return v;
|
|
12362
|
+
const io = createInterface({ input: keyInput, output: process.stderr });
|
|
12363
|
+
try {
|
|
12364
|
+
const lines = [yellow(" " + title)];
|
|
12365
|
+
q2.options.forEach((o, i) => lines.push(` ${i + 1}) ${o.label}${o.description ? dim(" \u2014 " + o.description) : ""}`));
|
|
12366
|
+
const ans = (await io.question(lines.join("\n") + "\n > ")).trim();
|
|
12367
|
+
const n = Number(ans);
|
|
12368
|
+
return Number.isInteger(n) && q2.options[n - 1] ? q2.options[n - 1].label : ans;
|
|
12369
|
+
} finally {
|
|
12370
|
+
io.close();
|
|
12371
|
+
}
|
|
12260
12372
|
}
|
|
12261
|
-
}
|
|
12373
|
+
} : {}
|
|
12262
12374
|
};
|
|
12263
12375
|
}
|
|
12264
12376
|
function summarizeResult(name, text) {
|
|
@@ -12621,6 +12733,11 @@ function resolvePermMode(args, interactiveCapable) {
|
|
|
12621
12733
|
}
|
|
12622
12734
|
function makeAskResolver(cwd) {
|
|
12623
12735
|
return async (call) => {
|
|
12736
|
+
if (!canPrompt) {
|
|
12737
|
+
err(dim(` \u26A0 ${call.name} needs approval but no terminal is attached \u2014 denied. Use --yes, or an allow rule in .agent/permissions.json.
|
|
12738
|
+
`));
|
|
12739
|
+
return { decision: "deny" };
|
|
12740
|
+
}
|
|
12624
12741
|
const name = call.name === "McpCall" && typeof call.args?.name === "string" ? String(call.args.name) : call.name;
|
|
12625
12742
|
const tgt = call.args?.path ? ` on ${call.args.path}` : call.args?.command ? `: ${String(call.args.command).slice(0, 60)}` : "";
|
|
12626
12743
|
const interactive = !!(process.stderr.isTTY && process.stdin.isTTY);
|
|
@@ -12851,6 +12968,8 @@ function jsonResult(res, session) {
|
|
|
12851
12968
|
tools: res.messages.slice(lastUser).filter((m) => m.role === "tool").length,
|
|
12852
12969
|
usage: res.usage,
|
|
12853
12970
|
sessionId: session.meta.id,
|
|
12971
|
+
// The parked question, machine-readable — the caller answers it and resumes `sessionId`.
|
|
12972
|
+
...res.finishReason === "needs_input" && res.question ? { question: res.question } : {},
|
|
12854
12973
|
...res.finishReason === "error" && res.error ? { error: res.error?.message ?? String(res.error) } : {}
|
|
12855
12974
|
};
|
|
12856
12975
|
}
|
|
@@ -12890,6 +13009,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
12890
13009
|
spinner.turnStart = t0;
|
|
12891
13010
|
spinner.start(sendFn ? "voice\u2026" : void 0);
|
|
12892
13011
|
const msgsBefore = agent.transcript.length;
|
|
13012
|
+
agent.options.host?.takeToolCount?.();
|
|
12893
13013
|
const host = agent.options.host;
|
|
12894
13014
|
const origNotify = host?.notify?.bind(host);
|
|
12895
13015
|
if (host && origNotify) host.notify = (e) => {
|
|
@@ -12931,13 +13051,13 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
12931
13051
|
const m = res.usageEstimated ? "~" : "";
|
|
12932
13052
|
const tok = res.usage?.totalTokens ? `${m}${(res.usage.totalTokens / 1e3).toFixed(1)}k tok \xB7 ${cost > 0 ? m + fmtUsd(cost) + " \xB7 " : ""}` : "";
|
|
12933
13053
|
const lastUser = res.messages.map((m2) => m2.role).lastIndexOf("user");
|
|
12934
|
-
const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length;
|
|
13054
|
+
const tools = res.messages.slice(lastUser).filter((m2) => m2.role === "tool").length + (agent.options.host?.takeToolCount?.() ?? 0);
|
|
12935
13055
|
const ok = res.finishReason === "stop";
|
|
12936
13056
|
const shortId = session.meta.id.replace(/^\d{8}-/, "");
|
|
12937
13057
|
const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
|
|
12938
13058
|
const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
|
|
12939
13059
|
if (!silentAbort)
|
|
12940
|
-
err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
|
|
13060
|
+
err("\n" + (process.stderr.isTTY ? "\r\x1B[0J" : "") + (ok ? green(" \u2713 done") : res.finishReason === "needs_input" ? yellow(" ? needs input") : red(` \u2717 ${res.finishReason}`)) + dim(` \xB7 ${res.steps} steps \xB7 ${tools} tools \xB7 ${tok}${secs}s \xB7 ${shortId}
|
|
12941
13061
|
`));
|
|
12942
13062
|
if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
|
|
12943
13063
|
if (res.finishReason === "error" && res.error) {
|
|
@@ -15393,7 +15513,7 @@ async function main() {
|
|
|
15393
15513
|
const session = startSession(args, store, agent, cwd);
|
|
15394
15514
|
process.once("SIGINT", () => activeTurn?.abort());
|
|
15395
15515
|
const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
|
|
15396
|
-
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
15516
|
+
if (cfg.reflectOnFailure && !ok && res?.finishReason !== "needs_input" && res && agent.options.memoryDir) {
|
|
15397
15517
|
const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
|
|
15398
15518
|
const slug2 = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
15399
15519
|
if (slug2) err(dim(` \u270E learned a lesson \u2192 ${slug2}
|
|
@@ -15405,7 +15525,7 @@ async function main() {
|
|
|
15405
15525
|
process.stdout.write(JSON.stringify(args.outputFormat === "stream-json" ? { type: "result", ...obj } : obj) + "\n");
|
|
15406
15526
|
}
|
|
15407
15527
|
worktreeCleanup?.();
|
|
15408
|
-
process.exit(ok ? 0 : 1);
|
|
15528
|
+
process.exit(ok ? 0 : res?.finishReason === "needs_input" ? 2 : 1);
|
|
15409
15529
|
}
|
|
15410
15530
|
await repl(args, ai, cfg, cwd);
|
|
15411
15531
|
worktreeCleanup?.();
|