@livx.cc/agentx 0.99.61 → 0.99.62
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/cli.d.ts +2 -0
- package/dist/cli.js +100 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ agentx --resume <id> "…" # resume a specific session
|
|
|
119
119
|
- **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.
|
|
120
120
|
- **Tab-completion** — `Tab` completes `/<command>` names and `@<path>` file/dir references (descends subdirs, dotfiles hidden unless typed) straight from the working tree.
|
|
121
121
|
- **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).
|
|
122
|
+
- **Headless duplex for host apps** — `agentx --voice --input-format stream-json --output-format stream-json` runs the same duplex engine behind an NDJSON pipe, for an app that owns its own mic/TTS (the Bare browser uses it). stdin: `{type:'user', text, id?, replaces?, cut?}` (`cut` = the host voice engine's `takeInterruptedReply()`, so "heard up to" / parked deliveries work exactly as in the CLI), `{type:'barge'}`, `{type:'cancel', task}`, `{type:'exit'}`. stdout: reflex `text`/`thinking` deltas, worker `speak` utterances, `filler`, `task` lifecycle events, `turn_done`, `revoice_done`, `exit`. `--append-system-prompt` goes to the workers, `--voice-system-prompt` to the voice model.
|
|
122
123
|
- **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. **Slow tools are jobs, not timeouts**: `timeoutMs` (default 30s) bounds protocol requests (`initialize`, `tools/list`) only; a `tools/call` still running after the soft foreground timeout (`foregroundMs.mcp`, 30s) returns `[still running] … background job job-N` while the request keeps going, and its result is delivered when it lands. The client sends a `progressToken` and reads progress over stdio and incrementally over HTTP/SSE; a call is cancelled (`notifications/cancelled`) only by `JobKill`, the stuck-job reaper, or its hard cap `callTimeoutMs` (per server, default 30 min).
|
|
123
124
|
|
|
124
125
|
## 🧬 It improves itself
|
package/dist/cli.d.ts
CHANGED
|
@@ -141,9 +141,11 @@ interface Args {
|
|
|
141
141
|
sessionId?: string;
|
|
142
142
|
fork?: boolean;
|
|
143
143
|
outputFormat: 'text' | 'json' | 'stream-json';
|
|
144
|
+
inputFormat?: 'stream-json';
|
|
144
145
|
allowedTools?: string[];
|
|
145
146
|
disallowedTools?: string[];
|
|
146
147
|
appendSystemPrompt?: string;
|
|
148
|
+
voiceSystemPrompt?: string;
|
|
147
149
|
addDirs?: string[];
|
|
148
150
|
print?: boolean;
|
|
149
151
|
debug?: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -10381,7 +10381,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
10381
10381
|
const rec = this.tasks.get(id);
|
|
10382
10382
|
if (res.finishReason === "aborted" || rec.status === "cancelled") {
|
|
10383
10383
|
rec.status = "cancelled";
|
|
10384
|
-
this.notify("task_cancelled", `task ${id} (${rec.label}) cancelled
|
|
10384
|
+
this.notify("task_cancelled", `task ${id} (${rec.label}) cancelled`, { id });
|
|
10385
10385
|
return;
|
|
10386
10386
|
}
|
|
10387
10387
|
if (res.finishReason === "error") {
|
|
@@ -10433,7 +10433,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
10433
10433
|
rec.status = "error";
|
|
10434
10434
|
rec.result = msg;
|
|
10435
10435
|
log20.warn(`task ${rec.id} failed: ${msg}`);
|
|
10436
|
-
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}
|
|
10436
|
+
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`, { id: rec.id });
|
|
10437
10437
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
10438
10438
|
this.dropParked(rec);
|
|
10439
10439
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -15733,6 +15733,7 @@ function parseArgs(argv) {
|
|
|
15733
15733
|
else if (x === "--allowedTools" || x === "--allowed-tools") a.allowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
15734
15734
|
else if (x === "--disallowedTools" || x === "--disallowed-tools") a.disallowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
|
|
15735
15735
|
else if (x === "--append-system-prompt") a.appendSystemPrompt = val(++i, x);
|
|
15736
|
+
else if (x === "--voice-system-prompt") a.voiceSystemPrompt = val(++i, x);
|
|
15736
15737
|
else if (x === "--add-dir") (a.addDirs ??= []).push(val(++i, x));
|
|
15737
15738
|
else if (x === "--reasoning") a.reasoning = parseReasoning(val(++i, x));
|
|
15738
15739
|
else if (x === "--session-id") a.sessionId = val(++i, x);
|
|
@@ -15745,6 +15746,10 @@ function parseArgs(argv) {
|
|
|
15745
15746
|
const f = argv[++i];
|
|
15746
15747
|
if (f !== "text" && f !== "json" && f !== "stream-json") throw new Error(`invalid --output-format: ${f ?? "(missing)"} (use text|json|stream-json)`);
|
|
15747
15748
|
a.outputFormat = f;
|
|
15749
|
+
} else if (x === "--input-format") {
|
|
15750
|
+
const f = argv[++i];
|
|
15751
|
+
if (f !== "stream-json") throw new Error(`invalid --input-format: ${f ?? "(missing)"} (use stream-json)`);
|
|
15752
|
+
a.inputFormat = f;
|
|
15748
15753
|
} else if (x === "--") {
|
|
15749
15754
|
rest.push(...argv.slice(i + 1));
|
|
15750
15755
|
break;
|
|
@@ -15757,7 +15762,9 @@ function parseArgs(argv) {
|
|
|
15757
15762
|
if (a.seed && !a.boddb) throw new Error("--seed only applies with --boddb (it seeds the database from cwd on first run)");
|
|
15758
15763
|
if (a.worktree && (a.vfs || a.boddb)) throw new Error("--worktree is a real-disk concept \u2014 incompatible with --vfs/--boddb");
|
|
15759
15764
|
if (a.duplex && (a.task || a.print)) throw new Error("--duplex is interactive-only (a conversational mode) \u2014 drop the task/-p");
|
|
15765
|
+
if (a.inputFormat && (!a.duplex || a.outputFormat !== "stream-json")) throw new Error("--input-format stream-json is headless duplex \u2014 use it with --duplex (or --voice) and --output-format stream-json");
|
|
15760
15766
|
if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
|
|
15767
|
+
if (a.voiceSystemPrompt && !a.duplex) throw new Error("--voice-system-prompt only applies with --duplex");
|
|
15761
15768
|
if (a.voiceModel && !a.duplex) throw new Error("--voice-model only applies with --duplex");
|
|
15762
15769
|
if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
|
|
15763
15770
|
if (!canPrompt && (a.ask || a.plan))
|
|
@@ -15807,6 +15814,7 @@ Flags:
|
|
|
15807
15814
|
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-120b)
|
|
15808
15815
|
--think-model <id> with --duplex: the premium deep-reasoning model (default anthropic/claude-opus-4-6)
|
|
15809
15816
|
--no-think with --duplex: disable the Think tier (Act handles everything)
|
|
15817
|
+
--voice-system-prompt <text> with --duplex: appended to the voice (reflex) prompt only (--append-system-prompt \u2192 workers)
|
|
15810
15818
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
15811
15819
|
--subagents allow the Task tool (spawn child agents)
|
|
15812
15820
|
--reasoning <e> extended thinking: off|low|medium|high or a token budget (anthropic/openai)
|
|
@@ -15816,6 +15824,8 @@ Flags:
|
|
|
15816
15824
|
--max-steps <n> step budget (default 30)
|
|
15817
15825
|
--max-tokens <n> token budget kill-switch (default 200000)
|
|
15818
15826
|
--timeout <sec> wall-clock kill-switch (default 120)
|
|
15827
|
+
--input-format stream-json with --duplex|--voice + --output-format stream-json: headless duplex for a host app
|
|
15828
|
+
(NDJSON turns/barge-ins on stdin, reflex text + worker speech + task events on stdout)
|
|
15819
15829
|
--output-format <f> headless output: text (default) | json (one result object) | stream-json (NDJSON events: {type:text|thinking}\u2026 then {type:result})
|
|
15820
15830
|
-v, --version print version and exit
|
|
15821
15831
|
-h, --help
|
|
@@ -17210,6 +17220,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
17210
17220
|
exitRequested = true;
|
|
17211
17221
|
})] }
|
|
17212
17222
|
});
|
|
17223
|
+
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
17213
17224
|
}
|
|
17214
17225
|
const face = dx ? dx.voice : agent;
|
|
17215
17226
|
const work = workerOptions ?? agent.options;
|
|
@@ -19208,6 +19219,88 @@ ${out}
|
|
|
19208
19219
|
disposeClaudeCodeSessions();
|
|
19209
19220
|
await closeMcp(mounted);
|
|
19210
19221
|
}
|
|
19222
|
+
async function duplexServe(args, ai, cfg, cwd) {
|
|
19223
|
+
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
19224
|
+
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join14(cwd, ".agent", "mcp-auth.json") }));
|
|
19225
|
+
installCancelGuards(mounted);
|
|
19226
|
+
const agent = await makeAgent(args, ai, cfg, mcpAgentTools(mounted, { quiet: true }), mounted.map((m) => m.name));
|
|
19227
|
+
const { host: _h, stream: _s, signal: _g, providerOptions: _po, ...wo } = agent.options;
|
|
19228
|
+
const host = {
|
|
19229
|
+
notify(e) {
|
|
19230
|
+
const k = String(e.kind);
|
|
19231
|
+
if (k === "text_delta") out({ type: "text", text: e.message });
|
|
19232
|
+
else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
|
|
19233
|
+
else if (k === "speak_utterance") out({ type: "speak", text: e.message });
|
|
19234
|
+
else if (k === "hold_filler") out({ type: "filler", text: e.message });
|
|
19235
|
+
else if (k === "revoice_done") {
|
|
19236
|
+
out({ type: "revoice_done" });
|
|
19237
|
+
persist();
|
|
19238
|
+
} else if (k.startsWith("task_")) out({ type: "task", kind: k.slice(5), id: e.data?.id, message: e.message, data: e.data });
|
|
19239
|
+
}
|
|
19240
|
+
};
|
|
19241
|
+
let persist = () => {
|
|
19242
|
+
};
|
|
19243
|
+
const dx = new DuplexAgent({
|
|
19244
|
+
ai,
|
|
19245
|
+
fs: agent.options.fs,
|
|
19246
|
+
memoryDir: agent.options.memoryDir,
|
|
19247
|
+
memoryUserDir: agent.options.memoryUserDir,
|
|
19248
|
+
...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
|
|
19249
|
+
actModel: agent.options.model,
|
|
19250
|
+
actOptions: { ...wo, planMode: false },
|
|
19251
|
+
providerOptionsFor: (m) => providerOptionsFor(m, cwd, cfg.mcpServers, mounted.map((x) => x.name)),
|
|
19252
|
+
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
19253
|
+
host,
|
|
19254
|
+
// --voice: the CLI voice register (progress asides, spoken budget). No emotion tags: the host's TTS may not speak them.
|
|
19255
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, spokenBudgetChars: 350 } : {},
|
|
19256
|
+
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
|
|
19257
|
+
});
|
|
19258
|
+
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
19259
|
+
const store = new SessionStore(cwd);
|
|
19260
|
+
const session = startSession(args, store, dx.voice, cwd);
|
|
19261
|
+
persist = () => {
|
|
19262
|
+
session.messages = dx.voice.transcript;
|
|
19263
|
+
session.meta.updated = Date.now();
|
|
19264
|
+
try {
|
|
19265
|
+
store.save(session);
|
|
19266
|
+
} catch (e) {
|
|
19267
|
+
log33.warn("duplex session save failed", e);
|
|
19268
|
+
}
|
|
19269
|
+
};
|
|
19270
|
+
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel });
|
|
19271
|
+
const done = new Promise((resolveDone) => {
|
|
19272
|
+
const rl = createInterface({ input: process.stdin });
|
|
19273
|
+
rl.on("line", (line) => {
|
|
19274
|
+
let m;
|
|
19275
|
+
try {
|
|
19276
|
+
m = JSON.parse(line);
|
|
19277
|
+
} catch {
|
|
19278
|
+
return;
|
|
19279
|
+
}
|
|
19280
|
+
if (m.type === "barge") {
|
|
19281
|
+
activeTurn?.abort();
|
|
19282
|
+
dx.parkInFlightDeliveries();
|
|
19283
|
+
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
19284
|
+
else if (m.type === "exit") rl.close();
|
|
19285
|
+
else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
|
|
19286
|
+
const { reply, delivery, heard } = splitCut(m.cut ?? null);
|
|
19287
|
+
if (delivery || reply) dx.parkCutDelivery(delivery, !!reply, heard);
|
|
19288
|
+
const note = reply && reply.full.length - reply.heard.length > 40 ? interruptionNote(reply) : "";
|
|
19289
|
+
const id = m.id ? String(m.id) : void 0;
|
|
19290
|
+
void runTurn(dx.voice, store, session, m.text + note, void 0, cwd, (c) => dx.send(c, { id, replaces: m.replaces ? String(m.replaces) : void 0 })).then(({ res }) => out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error" })).catch((e) => {
|
|
19291
|
+
log33.error("duplex turn failed", e);
|
|
19292
|
+
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
19293
|
+
});
|
|
19294
|
+
}
|
|
19295
|
+
});
|
|
19296
|
+
rl.on("close", () => resolveDone());
|
|
19297
|
+
});
|
|
19298
|
+
await done;
|
|
19299
|
+
for (const t of dx.tasks.values()) if (t.status === "running") dx.cancelTask(t.id);
|
|
19300
|
+
activeTurn?.abort();
|
|
19301
|
+
persist();
|
|
19302
|
+
await closeMcp(mounted);
|
|
19303
|
+
}
|
|
19211
19304
|
function readAllStdin() {
|
|
19212
19305
|
return new Promise((res) => {
|
|
19213
19306
|
let data = "";
|
|
@@ -19329,6 +19422,11 @@ async function main() {
|
|
|
19329
19422
|
worktreeCleanup?.();
|
|
19330
19423
|
process.exit(ok ? 0 : res?.finishReason === "needs_input" ? 2 : 1);
|
|
19331
19424
|
}
|
|
19425
|
+
if (args.inputFormat) {
|
|
19426
|
+
await duplexServe(args, ai, cfg, cwd);
|
|
19427
|
+
worktreeCleanup?.();
|
|
19428
|
+
process.exit(0);
|
|
19429
|
+
}
|
|
19332
19430
|
await repl(args, ai, cfg, cwd);
|
|
19333
19431
|
worktreeCleanup?.();
|
|
19334
19432
|
}
|