@livx.cc/agentx 0.98.1 → 0.99.1

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 CHANGED
@@ -35,7 +35,7 @@ Claude Code is the floor; running isolated, on the edge, or hybrid is the ceilin
35
35
  Plus things Claude Code simply doesn't do:
36
36
 
37
37
  - **Runs where CC can't** — the *same* agent loop runs on real disk, an in-memory **sandbox**, the **browser/edge** (no Node, no `/bin/sh`), or a **database-backed** workspace. Swap the filesystem, not the agent.
38
- - **Keyless web search, built in** — `WebSearch` works in any deployment with no API key (DuckDuckGo; auto-upgrades to Tavily if you set one). CC's search is Anthropic-server-bound.
38
+ - **Keyless web search, built in** — `WebSearch` works in any deployment with no API key (DuckDuckGo; auto-upgrades to Firecrawl or Tavily if you set a key). Plus optional `WebSearchAnthropic` — Anthropic's native search via a cheap model, on by default when an Anthropic key is set (API-billed; opt out with `anthropicWebSearch: false`).
39
39
  - **Context-safe by default** — a 1 MB `Grep`/`Read`/MCP result is auto-paginated and can't blow the window; buried detail is recovered via a cheap context-isolated `Ask` peek — **~5.3× cheaper and more accurate** than re-fetching, in a head-to-head.
40
40
  - **It improves its own efficiency** — an autonomous evolution loop cut its own tool-use **~50% (32 → 15** on the core suite, denoised), self-discovered, not hand-tuned — the same lever behind the efficiency lead above.
41
41
 
@@ -77,7 +77,7 @@ console.log(res.finishReason, await fs.readFile('/src/x.ts'));
77
77
  - **`Edit`** — exact unique-substring replace, with a read-before-edit staleness guard.
78
78
  - **`Grep`/`Glob`/`Write`/`MultiEdit`** — structured, typed results straight from the VFS (no `bash` parsing). The selectable tool set the self-evolution loop mutates over.
79
79
  - **`TodoWrite`** — a planning scratchpad; **`Task`** — spawn a depth-limited child agent over the VFS (`subagents: true`); **`SlashCommand`** — reusable prompt templates from `<dir>/*.md` (`commandsDir`); plus a real **MCP client** (`src/mcp.client.ts`, node-only — stdio/HTTP JSON-RPC handshake + discovery) that feeds the edge-safe **MCP adapter** (`mcpToolsToAgentTools`), so any MCP server's tools become agent tools.
80
- - **`WebFetch`/`WebSearch`** — fetch a URL as readable text, or search the web. **Keyless by default** (WebSearch uses DuckDuckGo; auto-upgrades to Tavily when `TAVILY_API_KEY` is set) and **auto-enabled in the CLI**. Factory-built with an injectable `fetch`, so they stay edge-portable and testable. (In the library they're opt-in by name: `tools: [...,'WebSearch']`.)
80
+ - **`WebFetch`/`WebSearch`** — fetch a URL as readable text, or search the web. **Keyless by default** (WebSearch uses DuckDuckGo; `provider: 'auto'` upgrades by key presence — Firecrawl `FIRECRAWL_API_KEY` > Tavily `TAVILY_API_KEY`) and **auto-enabled in the CLI**. Factory-built with an injectable `fetch`, so they stay edge-portable and testable. (In the library they're opt-in by name: `tools: [...,'WebSearch']`.) **`WebSearchAnthropic`** is a separate provider-pinned tool — Anthropic's native search relayed through a cheap model (`claude-haiku-4-5`); CLI-default-on when an Anthropic key resolves, API-billed per call, opt out via `anthropicWebSearch: false`.
81
81
  - **Oversized-output pagination** — any tool result over a byte ceiling (`maxToolResultBytes`, default 60k) is cropped to page 1 with a marker (refine the query / read further), so one big `Grep`/`Read`/MCP/web result can't blow the context window. In the CLI (**on by default**; `--no-scratch` to disable) the full output instead spills **losslessly** to a **scratch** file and the model recovers specifics via `Grep`/`Read` or **`Ask`** — a cheap, context-isolated peek that returns just the answer (the raw blob never re-enters context).
82
82
 
83
83
  ## Agentic subsystems
package/dist/cli.js CHANGED
@@ -645,13 +645,48 @@ function formatHits(hits) {
645
645
  ${r.url}
646
646
  ${r.snippet.replace(/\s+/g, " ").slice(0, 240)}`).join("\n\n");
647
647
  }
648
+ async function firecrawlSearch(q2, opts) {
649
+ const res = await opts.fetch(opts.endpoint, {
650
+ method: "POST",
651
+ signal: opts.signal,
652
+ headers: { authorization: `Bearer ${opts.key}`, "content-type": "application/json" },
653
+ body: JSON.stringify({ query: q2, limit: opts.maxResults })
654
+ });
655
+ if (!res.ok) return `Error: Firecrawl search returned ${res.status} ${res.statusText}`;
656
+ const data = await res.json();
657
+ const results = Array.isArray(data?.data) ? data.data.slice(0, opts.maxResults) : [];
658
+ return formatHits(results.map((r) => ({ title: r.title ?? "(untitled)", url: r.url ?? "", snippet: String(r.description ?? r.markdown ?? "") })));
659
+ }
660
+ async function anthropicSearch(q2, opts) {
661
+ const res = await opts.fetch("https://api.anthropic.com/v1/messages", {
662
+ method: "POST",
663
+ signal: opts.signal,
664
+ headers: { "x-api-key": opts.key, "anthropic-version": "2023-06-01", "content-type": "application/json" },
665
+ body: JSON.stringify({
666
+ model: opts.model,
667
+ max_tokens: 1024,
668
+ // Basic variant: Haiku-tier doesn't support the _20260209 dynamic-filtering variant (Opus 4.6+/Sonnet 4.6 only).
669
+ tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }],
670
+ messages: [{ role: "user", content: `Search the web for: ${q2}
671
+
672
+ Return only the relevant findings as concise bullet points, each with its source URL in parentheses. Do not add a preamble, conclusion, opinion, or commentary. If sources conflict, list each claim with its source rather than resolving it.` }]
673
+ })
674
+ });
675
+ if (!res.ok) return `Error: Anthropic search returned ${res.status} ${res.statusText}`;
676
+ const data = await res.json();
677
+ if (data?.stop_reason === "refusal") return "Error: Anthropic search refused the query";
678
+ let text = "";
679
+ for (const block of data?.content ?? []) if (block?.type === "text") text += block.text;
680
+ return text.trim() || "(no results)";
681
+ }
648
682
  function makeWebSearchTool(options = {}) {
649
683
  const tavilyEndpoint = options.endpoint ?? "https://api.tavily.com/search";
684
+ const firecrawlEndpoint = options.firecrawlEndpoint ?? "https://api.firecrawl.dev/v1/search";
650
685
  const maxResults = options.maxResults ?? 5;
651
686
  const timeoutMs = options.timeoutMs ?? 15e3;
652
687
  return {
653
- name: "WebSearch",
654
- description: "Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic \u2014 then WebFetch a result URL to read it in full.",
688
+ name: options.name ?? "WebSearch",
689
+ description: options.description ?? "Search the web by query; returns ranked results (title, URL, snippet). Use to look things up, find pages, or research a topic \u2014 then WebFetch a result URL to read it in full.",
655
690
  parameters: { type: "object", required: ["query"], properties: { query: { type: "string" } } },
656
691
  async run({ query }) {
657
692
  const doFetch = options.fetch ?? globalThis.fetch;
@@ -659,11 +694,22 @@ function makeWebSearchTool(options = {}) {
659
694
  const q2 = String(query ?? "").trim();
660
695
  if (!q2) return "Error: empty query";
661
696
  const key = options.apiKey ?? process.env.TAVILY_API_KEY;
697
+ const fcKey = options.firecrawlApiKey ?? process.env.FIRECRAWL_API_KEY;
662
698
  const provider = options.provider ?? "auto";
663
- const useTavily = provider === "tavily" || provider === "auto" && !!key;
699
+ const useFirecrawl = provider === "firecrawl" || provider === "auto" && !!fcKey;
700
+ const useTavily = provider === "tavily" || provider === "auto" && !useFirecrawl && !!key;
664
701
  const ctl = new AbortController();
665
702
  const timer = setTimeout(() => ctl.abort(), timeoutMs);
666
703
  try {
704
+ if (provider === "anthropic") {
705
+ const akey = options.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY;
706
+ if (!akey) return "Error: WebSearchAnthropic requires ANTHROPIC_API_KEY (set in env)";
707
+ return await anthropicSearch(q2, { key: akey, model: options.model ?? "claude-haiku-4-5", fetch: doFetch, signal: ctl.signal });
708
+ }
709
+ if (useFirecrawl) {
710
+ if (!fcKey) return "Error: Firecrawl provider selected but FIRECRAWL_API_KEY is not set";
711
+ return await firecrawlSearch(q2, { key: fcKey, endpoint: firecrawlEndpoint, maxResults, fetch: doFetch, signal: ctl.signal });
712
+ }
667
713
  if (useTavily) {
668
714
  if (!key) return "Error: Tavily provider selected but TAVILY_API_KEY is not set";
669
715
  const res2 = await doFetch(tavilyEndpoint, {
@@ -692,7 +738,10 @@ function makeWebSearchTool(options = {}) {
692
738
  }
693
739
  };
694
740
  }
695
- var log2, _dnsLookup, webFetchTool, webSearchTool;
741
+ function makeWebSearchAnthropicTool(opts = {}) {
742
+ return makeWebSearchTool({ provider: "anthropic", name: "WebSearchAnthropic", description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });
743
+ }
744
+ var log2, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
696
745
  var init_tools_web = __esm({
697
746
  "src/tools.web.ts"() {
698
747
  "use strict";
@@ -700,6 +749,8 @@ var init_tools_web = __esm({
700
749
  log2 = forComponent("web");
701
750
  webFetchTool = makeWebFetchTool();
702
751
  webSearchTool = makeWebSearchTool();
752
+ ANTHROPIC_SEARCH_DESC = "High-quality web search via Anthropic's native search index. Returns concise, sourced findings \u2014 one claim per line, each with its source URL. Prefer this over WebSearch when accuracy and citations matter; it is slower (~3\u20138s) and bills the Anthropic API account per call (one cheap-model turn + search fee).";
753
+ webSearchAnthropicTool = makeWebSearchAnthropicTool();
703
754
  }
704
755
  });
705
756
 
@@ -925,7 +976,7 @@ function defaultTools() {
925
976
  return [bashTool, readTool, editTool];
926
977
  }
927
978
  function toolRegistry() {
928
- const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool];
979
+ const all = [bashTool, readTool, editTool, grepTool, globTool, writeTool, multiEditTool, applyEditsTool, repoMapTool, reviewTool(), todoWriteTool, webFetchTool, webSearchTool, webSearchAnthropicTool];
929
980
  return Object.fromEntries(all.map((t) => [t.name, t]));
930
981
  }
931
982
  function toolsByName(names) {
@@ -4937,6 +4988,8 @@ var DuplexAgent = class _DuplexAgent {
4937
4988
  // briefs dispatched this turn (detect identical re-dispatch)
4938
4989
  spokeThisTurn = false;
4939
4990
  // any non-empty text_delta streamed this turn
4991
+ heldThisTurn = false;
4992
+ // Hold called this turn → turn is INTENTIONALLY silent (suppress reflex text + no dead-air ack)
4940
4993
  nudging = false;
4941
4994
  // re-ack pass in flight: block ALL tools, prevent recursion
4942
4995
  reflexBuf = "";
@@ -4999,6 +5052,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4999
5052
  confirm: host.confirm ? (p, m) => host.confirm(p, m) : void 0,
5000
5053
  notify: (ev) => {
5001
5054
  if (ev?.kind === "text_delta" && typeof ev.message === "string") {
5055
+ if (this.heldThisTurn) return;
5002
5056
  if (this.fabricationCut) return;
5003
5057
  const msg = ev.message;
5004
5058
  this.reflexBuf += msg;
@@ -5068,6 +5122,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
5068
5122
  this.turnDispatched = false;
5069
5123
  this.turnBriefs.clear();
5070
5124
  this.spokeThisTurn = false;
5125
+ this.heldThisTurn = false;
5071
5126
  this.reflexBuf = "";
5072
5127
  this.reflexForwarded = 0;
5073
5128
  this.fabricationCut = false;
@@ -5096,7 +5151,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
5096
5151
  * voice) and emits an empty `final`, so no text_delta ever streams. Both ship silence; both repair.
5097
5152
  * Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
5098
5153
  get silentTurn() {
5099
- return !!this.options.host && !this.spokeThisTurn;
5154
+ return !!this.options.host && !this.spokeThisTurn && !this.heldThisTurn;
5100
5155
  }
5101
5156
  /** A turn that voiced nothing is dead air. Re-prompt the reflex ONCE so the LLM itself voices a short
5102
5157
  * line (no template). If it STILL says nothing, fall back to a minimal line so silence never ships.
@@ -5657,6 +5712,7 @@ Another agent just implemented the above. Independently check the CURRENT state
5657
5712
  }
5658
5713
  },
5659
5714
  run: async ({ filler }) => {
5715
+ this.heldThisTurn = true;
5660
5716
  if (filler) this.notify("hold_filler", String(filler));
5661
5717
  return "Holding \u2014 listening for the rest of the user's thought. Do not respond further this turn.";
5662
5718
  }
@@ -7408,8 +7464,12 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
7408
7464
  return { systemPrompt: basePrompt + "\n\n" + extra };
7409
7465
  })(),
7410
7466
  tools: (() => {
7411
- const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
7467
+ const requested = o.tools ?? DEFAULT_TOOLS;
7468
+ const base = toolsByName([...requested, ...autoWebTools()]);
7412
7469
  const tail = [...o.extraTools ?? []];
7470
+ if (o.anthropicWebSearch !== false && o.anthropicKey && !requested.includes("WebSearchAnthropic")) {
7471
+ tail.push(makeWebSearchAnthropicTool({ anthropicApiKey: o.anthropicKey }));
7472
+ }
7413
7473
  if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
7414
7474
  tail.push(makeNotifyTool());
7415
7475
  if (!realShell.length) return [...base, ...tail];
@@ -10805,7 +10865,7 @@ Providers: set any of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ
10805
10865
  Env files: .env (CWD, bun auto-loads) > install-dir .env > ~/.agent/.env (user-wide).
10806
10866
  Bodify secrets: set BODIFY_API_KEY + BODIFY_APP_ID (in ~/.agent/.env) to pull keys from a Bodify app.
10807
10867
  Config: ./.agent/config.{ts,js,json} (project) or ~/.agent/config.* (user).
10808
- export default { model, maxSteps, reasoning, permissionMode, editorMode, tools, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
10868
+ export default { model, maxSteps, reasoning, permissionMode, editorMode, tools, anthropicWebSearch, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
10809
10869
  timeoutMs, maxRepeats, maxToolCalls, keepToolOutputs, maxContextTokens,
10810
10870
  learnFromMistakes, reflectOnFailure, budget: {\u2026} }
10811
10871
  hooks: { preToolUse|postToolUse|onStop: [{ tool?, command, block? }] } \u2014 shell hooks
@@ -11405,7 +11465,10 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
11405
11465
  learnFromMistakes: cfg.learnFromMistakes,
11406
11466
  // Forwarded to cursor/* delegations for environment parity (chat-model providers ignore it).
11407
11467
  // Raw config (pre-OAuth): unresolved-oauth http servers are skipped by the cursor mapper.
11408
- mcpServers: cfg.mcpServers
11468
+ mcpServers: cfg.mcpServers,
11469
+ // Gates + powers default-on WebSearchAnthropic. Same precedence as the main client (env wins).
11470
+ anthropicKey: apiKeysFromEnv().anthropic ?? cfg.apiKeys?.anthropic,
11471
+ anthropicWebSearch: cfg.anthropicWebSearch
11409
11472
  };
11410
11473
  }
11411
11474
  async function makeAgent(args, ai, cfg, extraTools = []) {