@livx.cc/agentx 0.99.20 → 0.99.22
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 +6 -4
- package/dist/cli.js +54 -21
- 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
|
|
@@ -236,7 +236,8 @@ interface PermMode {
|
|
|
236
236
|
* Default permission posture — CC-inspired: **ask when a human can answer, allow when unattended.**
|
|
237
237
|
* Pure + exported so the decision is unit-testable without a TTY. Precedence:
|
|
238
238
|
* --yes → allow (explicit trust, no prompts)
|
|
239
|
-
* --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)
|
|
240
241
|
* interactive (TTY & not json) → ask (the safe default)
|
|
241
242
|
* else (headless/piped) → allow, with a one-line notice so it's never silent.
|
|
242
243
|
*/
|
|
@@ -271,8 +272,9 @@ declare function expandMentions(fs: IFilesystem, line: string): Promise<{
|
|
|
271
272
|
/** The headless `--output-format json` result object for a turn. */
|
|
272
273
|
declare function jsonResult(res: RunResult, session: SessionData): {
|
|
273
274
|
error?: any;
|
|
275
|
+
question?: UserQuestion | undefined;
|
|
274
276
|
ok: boolean;
|
|
275
|
-
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";
|
|
276
278
|
text: string;
|
|
277
279
|
steps: number;
|
|
278
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
|
}
|
|
@@ -12070,6 +12082,8 @@ function parseArgs(argv) {
|
|
|
12070
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)");
|
|
12071
12083
|
if (a.voiceModel && !a.duplex) throw new Error("--voice-model only applies with --duplex");
|
|
12072
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.`);
|
|
12073
12087
|
return a;
|
|
12074
12088
|
}
|
|
12075
12089
|
var HELP = `agentx \u2014 agent runtime CLI
|
|
@@ -12097,7 +12111,8 @@ Flags:
|
|
|
12097
12111
|
--harden OS-sandbox the real shell (sandbox-exec/bwrap): writes confined to cwd+tmp,
|
|
12098
12112
|
outbound network blocked; --harden-net keeps network allowed
|
|
12099
12113
|
--plan plan mode: edits blocked until you approve a plan
|
|
12100
|
-
--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)
|
|
12101
12116
|
--yes, -y auto-approve mutating tools (no prompts) \u2014 for trusted/unattended runs
|
|
12102
12117
|
--update check for updates and install if available (no keys needed)
|
|
12103
12118
|
--no-update-check skip the automatic update check on startup
|
|
@@ -12129,6 +12144,10 @@ Flags:
|
|
|
12129
12144
|
|
|
12130
12145
|
Prompts may reference files with @path (e.g. "explain @src/Agent.ts") \u2014 they're inlined.
|
|
12131
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
|
+
|
|
12132
12151
|
Providers: set any of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY.
|
|
12133
12152
|
Env files: .env (CWD, bun auto-loads) > install-dir .env > ~/.agent/.env (user-wide).
|
|
12134
12153
|
Bodify secrets: set BODIFY_API_KEY + BODIFY_APP_ID (in ~/.agent/.env) to pull keys from a Bodify app.
|
|
@@ -12330,21 +12349,28 @@ function makeHost(format = "text", opts) {
|
|
|
12330
12349
|
io.close();
|
|
12331
12350
|
}
|
|
12332
12351
|
},
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
const
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
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
|
+
}
|
|
12346
12372
|
}
|
|
12347
|
-
}
|
|
12373
|
+
} : {}
|
|
12348
12374
|
};
|
|
12349
12375
|
}
|
|
12350
12376
|
function summarizeResult(name, text) {
|
|
@@ -12707,6 +12733,11 @@ function resolvePermMode(args, interactiveCapable) {
|
|
|
12707
12733
|
}
|
|
12708
12734
|
function makeAskResolver(cwd) {
|
|
12709
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
|
+
}
|
|
12710
12741
|
const name = call.name === "McpCall" && typeof call.args?.name === "string" ? String(call.args.name) : call.name;
|
|
12711
12742
|
const tgt = call.args?.path ? ` on ${call.args.path}` : call.args?.command ? `: ${String(call.args.command).slice(0, 60)}` : "";
|
|
12712
12743
|
const interactive = !!(process.stderr.isTTY && process.stdin.isTTY);
|
|
@@ -12937,6 +12968,8 @@ function jsonResult(res, session) {
|
|
|
12937
12968
|
tools: res.messages.slice(lastUser).filter((m) => m.role === "tool").length,
|
|
12938
12969
|
usage: res.usage,
|
|
12939
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 } : {},
|
|
12940
12973
|
...res.finishReason === "error" && res.error ? { error: res.error?.message ?? String(res.error) } : {}
|
|
12941
12974
|
};
|
|
12942
12975
|
}
|
|
@@ -13024,7 +13057,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
13024
13057
|
const didWork = agent.transcript.slice(msgsBefore).some((m2) => m2.role === "assistant" || m2.role === "tool");
|
|
13025
13058
|
const silentAbort = res.finishReason === "aborted" && !res.usage?.totalTokens && !didWork;
|
|
13026
13059
|
if (!silentAbort)
|
|
13027
|
-
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}
|
|
13028
13061
|
`));
|
|
13029
13062
|
if (!silentAbort && process.stderr.isTTY && Date.now() - t0 > 1e4) err("\x07");
|
|
13030
13063
|
if (res.finishReason === "error" && res.error) {
|
|
@@ -15480,7 +15513,7 @@ async function main() {
|
|
|
15480
15513
|
const session = startSession(args, store, agent, cwd);
|
|
15481
15514
|
process.once("SIGINT", () => activeTurn?.abort());
|
|
15482
15515
|
const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
|
|
15483
|
-
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
15516
|
+
if (cfg.reflectOnFailure && !ok && res?.finishReason !== "needs_input" && res && agent.options.memoryDir) {
|
|
15484
15517
|
const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
|
|
15485
15518
|
const slug2 = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
15486
15519
|
if (slug2) err(dim(` \u270E learned a lesson \u2192 ${slug2}
|
|
@@ -15492,7 +15525,7 @@ async function main() {
|
|
|
15492
15525
|
process.stdout.write(JSON.stringify(args.outputFormat === "stream-json" ? { type: "result", ...obj } : obj) + "\n");
|
|
15493
15526
|
}
|
|
15494
15527
|
worktreeCleanup?.();
|
|
15495
|
-
process.exit(ok ? 0 : 1);
|
|
15528
|
+
process.exit(ok ? 0 : res?.finishReason === "needs_input" ? 2 : 1);
|
|
15496
15529
|
}
|
|
15497
15530
|
await repl(args, ai, cfg, cwd);
|
|
15498
15531
|
worktreeCleanup?.();
|