agent: --search-engine, and say when the engine has no key

The search engine could only be set from the REPL's `/searchEngine`, so a
one-shot run had no way to pin one -- and a benchmark that wants its
results to mean something has to record which API answered.

`/searchEngine` also carried the only warning about key state, which is
the half that matters more. A keyless engine is not an error and starts
fine, so a run that silently falls back to a rate-limited public endpoint
looks exactly like a working one until every search begins failing, and
then it looks like a bad agent. Both messages now fire wherever the
engine is resolved, not just from the command.

`resolveSearchEngine`'s doc comment said there was no CLI flag. There is
one now.
This commit is contained in:
Adrià Arrufat committed 2026-09-22 15:50:15 +02:00
1 parent 1c9b1b8bc2
commit 92a6b698fc
4 files changed
+19 -3

No files matched your search

+2
View File
@@ -347,6 +347,7 @@ pub const AiProvider = std.meta.Tag(zenai.provider.Client);
/// in `Agent.init` (explicit flag > remembered > mode default), so there is
/// no Config-level accessor like `agentVerbosity`.
pub const Effort = zenai.provider.Effort;
pub const SearchEngine = @import("browser/tools.zig").SearchEngine;
/// Controls how chatty `agent` mode is on stderr.
pub const AgentVerbosity = enum {
@@ -470,6 +471,7 @@ const Commands = cli.Builder(.{
.{ .name = "attach", .short = 'a', .type = []const u8, .multiple = true },
.{ .name = "verbosity", .type = ?AgentVerbosity },
.{ .name = "effort", .type = ?Effort },
.{ .name = "search_engine", .type = ?SearchEngine },
.{ .name = "list_models", .type = bool },
.{ .name = "no_llm", .type = bool },
},
+9 -1
View File
@@ -289,7 +289,15 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent
const effort = settings.resolveEffort(opts, remembered, will_repl, if (resolved) |r| r.credential.provider else null);
const verbosity = settings.resolveVerbosity(opts, remembered);
const stream_enabled = settings.resolveStream(remembered);
browser_tools.search_engine = settings.resolveSearchEngine(remembered);
browser_tools.search_engine = opts.search_engine orelse settings.resolveSearchEngine(remembered);
// Only the REPL's `/searchEngine` used to say this. A keyless engine that
// has hit its cap fails every search, and without a word here that reads
// as a bad agent rather than a missing key.
if (browser_tools.searchKeyStatus(browser_tools.search_engine)) |key| switch (key.state) {
.set => {},
.keyless => log.info(.app, "keyless search endpoint", .{ .env_var = key.env_var, .limit = "rate-limited per client IP" }),
.missing => log.warn(.app, "search key missing", .{ .env_var = key.env_var, .engine = @tagName(browser_tools.search_engine) }),
};
if (resolved) |r| {
if (r.source == .picked) {
+2 -2
View File
@@ -369,8 +369,8 @@ pub fn resolveStream(remembered: ?Remembered) bool {
return true;
}
/// Precedence: remembered `.lp-agent.zon` value > default (auto). No CLI
/// flag — the REPL `/searchEngine` command sets and persists it.
/// Precedence: `--search-engine` > remembered `.lp-agent.zon` value >
/// default (auto). The REPL `/searchEngine` command also sets and persists it.
pub fn resolveSearchEngine(remembered: ?Remembered) lp.tools.SearchEngine {
if (remembered) |r| if (r.search_engine) |e| return e;
return .auto;
+6
View File
@@ -297,6 +297,12 @@
\\ it to PATH, instead of printing the answer. Replay it later with
\\ `run PATH` (no LLM calls). Overwrites PATH if it exists.
\\ Requires --task.
\\ --search-engine <ENGINE>
\\ Which web search API the `search` tool uses. Default: auto,
\\ which takes the first with a key in the environment, in the
\\ order brave, tavily, exa, keenable. `keenable` also works with
\\ no key at all, through a rate-limited public endpoint.
\\ Allowed values: auto, brave, tavily, exa, keenable.
\\ --system-prompt <STRING>
\\ Override the default system prompt.
\\ --task <STRING>