Commit Graph
507 Commits
Author SHA1 Message Date
Adrià Arrufat 728f61a80d log: check the message rules at comptime
logToErased asserts that a log message is at most 30 characters of
plain text, but only in a debug build and only once the line actually
runs. A message on a rare path therefore ships fine and then panics on
whoever first reaches it: `serve --host 0.0.0.0` without
--advertise-host crashed on startup in every debug build, because
"advertising loopback for wildcard bind" is 38 characters.

Every message is a literal, so make the six wrappers take a comptime
msg and apply the same two rules through @compileError. The runtime
check stays as the backstop for the paths the compiler does not
analyse for the current target, and now reads the same constant.

Eleven messages were over the limit; shorten them. The detail already
lives in the kv pairs in each case. renderFailed takes its message as
comptime now, the only call site that passed a runtime one.

Note the check only covers code analysed for the target being built:
the two in Certificates.zig sit in an OS switch prong that Linux never
compiles, and were found by scanning the source rather than by the
compiler.
2026-09-23 11:20:42 +08:00
Adrià Arrufat 290e4f81d9 agent: simplify REPL command dispatch and provider bookkeeping
- `available_providers` holds the static enum tag names; drop the dupe
  loop, its errdefer and the per-string frees.
- `reconcileModel` returns `error.ModelNotAvailable` directly instead of
  a `use`/`abort` union the caller only mapped to that same error.
- `runCommand`/`printCommandResult` take `Command.ToolCall`; the caller
  already has it, so the unreachable "no tool mapping" branches go.
- `handleSave` reuses `rememberSavePath`, which now propagates its
  allocation failure so a first save under OOM warns instead of
  unwrapping a null `save_path`.
- `SlashCommand.all_names` is a comptime `++` of the three name lists.
- `buildUserMessageParts` reads the attachment once for both text and
  image, and prepends the text part instead of copying the list.
- `printSeverity`/`formatBulletLine` use `allocPrint` and the shared
  `emitStderr`; drop an unreachable `ends_ws` check in `renderMetaHint`.
2026-09-18 00:45:55 +02:00
Adrià Arrufat 9b7225b580 agent: don't drop REPL tool lines or cut result text mid-codepoint
In a REPL whose stderr isn't a tty the spinner is disabled, so
`agentToolDone`'s `emitAbove` returned false and the `● [tool: …]` line
was discarded. `printToolOutcome` already fell back to a raw stderr write
in that case; share that fallback through `emitStderr`.

The non-REPL result line sliced `text` at a byte offset, which could
split a multi-byte codepoint and emit garbage. Truncate on a UTF-8
boundary instead.
2026-09-18 00:44:49 +02:00
Adrià Arrufat fc13928672 agent: document the OpenRouter and OrcaRouter providers
zenai now ships openrouter and orcarouter presets (OPENROUTER_API_KEY /
ORCAROUTER_API_KEY, provider-prefixed model ids). The provider enum is
derived from zenai, so only the help text, key hint, and README need
to name them.
2026-09-16 12:21:34 +02:00
Halil Durak 901e5eaf4d Agent: add lightpanda run -
Arbitrary JS code can be passed directly through stdin thanks to this.
2026-09-15 16:21:01 +03:00
Karl Seguin 2e6999f20b chore: make declarations private if they don't need to be public
This change is 99%  s/pub//   + a handful of dead code removal.
2026-09-10 14:42:09 +08:00
Adrià Arrufat 88d133a676 cli: suggest the closest flag or command on a typo
An unknown --flag now logs the nearest accepted spelling within two
edits as did_you_mean, painted green next to the red typo in the pretty
log format; logfmt stays plain. A bare first argument within two edits
of a command name is rejected with the same hint instead of being
fetched as a url.

The Levenshtein helper moves from SlashCommand into string.zig so the
agent REPL and the CLI share it, with the table widened to fit the
longest flag name.
2026-09-08 16:50:46 +02:00
Adrià Arrufat d8e804cdb4 Merge branch 'main' into repl-markdown-render
# Conflicts:
#	src/agent/Agent.zig
2026-08-31 16:48:43 +02:00
Adrià Arrufat f1bdd0019f Merge branch 'main' into tool-session 2026-08-28 15:14:56 +02:00
Adrià Arrufat 2899bfef4a agent: route REPL output per tool and align page tables of any size
Every data command went through printAssistant, so /markdown was styled by
accident while /tree, /getUrl and /evaluate strings had `*`/`_` eaten as
emphasis, and /markdown lost table alignment past Stream's 16 KiB buffer.

- printData renders only markdown as markdown; JSON is re-indented and the
  rest prints verbatim.
- render iterates the source directly and aligns tables from the slice;
  Fence holds the fence state shared with Stream.
- Links are OSC 8 hyperlinks around the label only, with the label
  inline-rendered; `![alt](url)` drops the `!`; escapes follow CommonMark
  so what the page dumper escapes is unescaped.
2026-08-28 14:38:27 +02:00
Adrià Arrufat d10029fb19 Merge branch 'main' into screenshot-tool 2026-08-27 21:13:36 +02:00
Adrià Arrufat 0865f4e337 agent: fold the browser quartet onto lp.ToolSession
Agent held the same Browser/Session/Notification/registry fields and
teardown order as mcp/Server.zig, plus its own "enableConsoleCapture
after every newSession" for /reset. ToolSession.restartSession() owns
that now; init is written in terms of it.

Claude-Session: https://claude.ai/code/session_01M6WGk8wZSE28efFQkYT9SK
2026-08-27 16:40:53 +02:00
Adrià Arrufat e70b316aea agent: use zenai's hasImage
zenai exports it now; drop the local copy and re-pin.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 45744407af screenshot: fit() owns the inline sizing; prune() owns image expiry
The renderer knows that layout reflows to the width and that height 0
means the whole content, so the bounding rule moves next to measure()
and only measures when the strip isn't already fixed: the default
viewport shot no longer pays a second layout pass. The limits are
constants spliced into the tool description, not a caller-tunable that
nothing tuned. expireImages runs inside prune so there is one end-of-
turn hook, and re-homes a stripped result instead of casting away const.
2026-08-27 15:03:19 +02:00
Adrià Arrufat c693979468 agent: bound what a screenshot costs the model
An inline screenshot is re-sent on every request for as long as it sits
in history, and a full-page render could reach 1920x16384. Inline images
are now rendered at most 1280 wide and 4096 tall (measured after the
reflow, so short pages aren't padded), files keep full size; the
conversation keeps only the newest two images, older tool results keep
their text with a note; the MCP transport releases its buffer after a
large response.
2026-08-27 15:03:19 +02:00
Adrià Arrufat cff32f6c04 agent: let the model see a REPL /screenshot; fold the tool-result adapters
The slash path's result has two consumers: the terminal, which can't
show an image, and the conversation, which can. Opt in when a model is
attached and forward the image through the same adapter the model-driven
path uses, and stop mapping a failed adapter to a text-only success.
MCP's ImageContent and CallToolResult take their payload type like
TextContent does; resolveScope reuses resolveTarget; needsLocator folds
into replayRequires.
2026-08-27 15:03:19 +02:00
Adrià Arrufat e6203ff1fe agent: hand the model inline screenshots
zenai tool results now carry image parts (lightpanda-io/zenai#12, #13),
so the model-driven tool path opts into inline images: a screenshot
without `path` reaches the model as text plus the PNG, on every backend.
The slash-command path still needs `path`, its result goes to the
terminal.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 193f15558c tools: declare image support at the call site, not in each consumer
Callers pass CallOpts.inline_image; execScreenshot rejects a path-less
call before navigating or rendering, which removes the per-consumer
guards and covers the model-driven tool path that had none. MCP image
content is a protocol type, the screenshot recording rule joins the
recorder's replayRequires predicate, and the viewport-to-Opts mapping,
node-scope ladder and save-path helpers are shared instead of copied.
2026-08-27 15:03:19 +02:00
Adrià Arrufat 56c6a0a7ab tools: add a screenshot tool
The PNG renderer was reachable from CDP and fetch --dump png only. The
tool renders the page or one node; with `path` it writes the file and
returns its location (agent, PandaScript, MCP), without it MCP returns
the image inline as base64 content. ToolResult carries the prepared
image so the transport streams it; the agent and script runtime reject
the inline form since their tool results are text. An inline screenshot
is not recorded, as it has no replayable form.
2026-08-27 15:03:19 +02:00
Adrià Arrufat ed99a5b79f agent: run the Terminal tests
Terminal.zig references md_term and prompt_assist from a test block, but
nothing referenced Terminal, so those 30-odd tests never ran. Adding it
to Agent.zig's discovery block surfaced one rotted test sink in
js_highlight.zig still using ArrayList.writer(), ported to
Io.Writer.Allocating.
2026-08-27 12:03:23 +02:00
Adrià Arrufat c3b8f43f5f search: resolve the engine key once, retries off in one place
`engineKey` (on `lp.environ().getPosix`) replaces the env-var wrapper and
the three hand-spelled set/keyless/missing decisions in the cascade, the
explicit path, and the REPL query. `.auto`'s "always a rung" invariant is
now a comptime assert instead of an unreachable sentinel. HTTP retries are
disabled in `apiSearch` for every path — the cascade or the model is the
retry, and the tool's `timeout` argument bounds one attempt — rather than
threaded through as a parameter the table no longer set. The cascade
sentence is one shared constant for the tool description and the REPL
help, and search gets its own default timeout instead of borrowing the
navigation one.
2026-08-25 09:43:06 +02:00
Adrià Arrufat 42c3cbf20b search: default to Keenable's keyless endpoint, drop the DuckDuckGo scrape
The DuckDuckGo HTML endpoint the search tool fell back to is disallowed by
its robots.txt, so it is removed: the `duckduckgo` engine, the scrape via
`performGoto`, and the tool's browser-side dependency (`execSearch` no
longer needs a session). In its place `.auto` is a single walk over the
API engines in table order — each when its key is set — with Keenable
last, keyed if `KEENABLE_API_KEY` is set and its public endpoint
otherwise. Search therefore always has a rung with zero configuration,
and a set key is never shadowed by a keyless retry.

Every search now goes through zenai's `std.http.Client`, which had no
timeout at all; a stalled response would have blocked the MCP server's
shared browser thread for every session. zenai now bounds each attempt
(lightpanda-io/zenai#10, pinned here), and the tool's `timeout` argument
drives it, defaulting to 10 s. Retries are a property of the call path:
off in the cascade, where the next rung is the retry, and the engine
default for an explicit engine.

Cleanups from the #3252 review: `keyless` is derived from the client's
`api_key` type instead of a hand-set table flag; `engineIndex` no longer
depends on enum declaration order; one `searchKeyStatus` query replaces
three pub helpers for the REPL; the engine order and env-var list in the
tool description and `/searchEngine` help are generated from the table;
an empty result title renders as its URL instead of `****`.
2026-08-25 09:35:26 +02:00
Ilya Bogin 6cbe30b088 search: add Keenable engine with a keyless rung above the DDG scrape
Adds keenable to the search tool's engines (KEENABLE_API_KEY, tried
after brave/tavily/exa in .auto). Unlike the other engines it also
answers without any key: its client routes an empty key to the public
endpoint (rate-limited per client IP), so .auto now tries that as the
last rung before the DuckDuckGo scrape, and an explicit
/searchEngine keenable works keyless instead of erroring. Engine table
entries gain init_options so the Keenable client can carry the
lightpanda attribution header.

Needs the zenai Keenable search client (lightpanda-io/zenai PR #9); the
zon pin points at that branch until it lands.
2026-08-23 15:42:06 +03:00
Adrià Arrufat d76ead300a browser: add exa search engine support 2026-08-18 21:10:45 +02:00
Adrià Arrufat e69aedea14 tools: treat backendNodeId 0 as omitted, add recovery hints to errors
Some models (gpt-5.x via codex) zero-fill optional tool params instead
of omitting them, so every tree/markdown/html call carried
backendNodeId: 0, failed with NodeNotFound, and got retried blind --
one 'go to hacker news' burned 19 tool calls and 18 full page loads.

Registry ids start at 1, so 0 can never name a real node. Make
0-means-omitted part of the tool contract: state it in the schema
descriptions and normalize it in parseValue. Required ids
(nodeDetails) are untouched.

In-band guidance alone was not enough -- the model re-emitted the
identical call even when the error spelled out the fix -- but keep it
for genuinely stale ids: NodeNotFound and FrameNotLoaded now carry an
actionable message in the agent, slash-command, and MCP paths instead
of a bare error name.
2026-08-06 14:20:34 +02:00
Adrià Arrufat c2fa38e516 Merge pull request #3075 from lightpanda-io/codex
agent: support subscription auth via Codex
2026-07-30 08:02:11 +02:00
Adrià Arrufat 97e26ae86e agent: add keep/login/logout menu for subscription providers 2026-07-29 10:07:52 +02:00
Adrià Arrufat 27b37e6c93 auth: simplify token refresh and json store helpers 2026-07-29 09:55:49 +02:00
Adrià Arrufat d7eba33fb0 auth: fallback to stale models.dev cache on fetch failure 2026-07-29 09:41:50 +02:00
Adrià Arrufat 5f76f7056a auth: log warnings on codex auth request failures 2026-07-29 09:38:53 +02:00
Adrià Arrufat a00b5dceb7 auth: parse device code interval as u32 2026-07-29 09:37:38 +02:00
Adrià Arrufat 939feeb9a3 auth: consider refreshable tokens in subscription check 2026-07-29 09:35:14 +02:00
Adrià Arrufat c8e7761fce agent/auth: parse models directly into arena 2026-07-29 09:32:11 +02:00
Adrià Arrufat 6070119ca8 auth: write auth store and model cache atomically 2026-07-29 09:26:56 +02:00
Adrià Arrufat 41e365b3f4 agent: move session banner printing to Terminal 2026-07-29 09:21:02 +02:00
Adrià Arrufat 0035d31fc9 codex: use std.json.fmt for poll body 2026-07-29 09:12:16 +02:00
Adrià Arrufat 6c28c3d6c9 auth: log warning on token persistence failure 2026-07-29 09:11:46 +02:00
Adrià Arrufat 52c2b90d30 auth: set token file permissions on creation 2026-07-29 09:07:51 +02:00
Adrià Arrufat 041ca0ba51 auth: pass allocator to token store save/delete functions 2026-07-29 09:00:46 +02:00
Karl Seguin 53ad5f53f6 Merge pull request #3067 from lightpanda-io/dead-imports
chore: remove unused imports
2026-07-29 06:45:30 +08:00
Adrià Arrufat 8b6f7fe064 agent: simplify credential and key ownership handling 2026-07-28 13:05:46 +02:00
Adrià Arrufat a53fbcc6eb agent: introduce KeyOwnership union for credentials
Consolidates owned_key, session, and unowned env key management into a
tagged union KeyOwnership to simplify resource lifetime handling.
2026-07-28 12:44:37 +02:00
Adrià Arrufat 00cdc3ba8d agent: update zenai and switch auth to interruptible HTTP 2026-07-28 11:17:08 +02:00
Adrià Arrufat 01a7b091db agent: map models_dev_id and filter tool-call models 2026-07-28 10:56:24 +02:00
Adrià Arrufat 819aee3c07 agent: support login cancellation and provider completion` 2026-07-28 10:28:18 +02:00
Adrià Arrufat d7cc090ca1 Merge branch 'main' into codex 2026-07-28 10:11:31 +02:00
Adrià Arrufat c9f7a11e42 Merge pull request #3073 from lightpanda-io/agent-tool-cap
agent: reduce tool output cap to 64KB except for extract
2026-07-28 09:44:39 +02:00
Adrià Arrufat a2e8ce8f50 agent: reduce tool output cap to 64KB except for extract 2026-07-28 09:17:34 +02:00
Karl Seguin a67fe9f90a chore: remove unused imports 2026-07-28 09:39:02 +08:00
Karl Seguin cc33bb38d1 Merge pull request #3061 from lightpanda-io/consistent-clocks
chore: make our use of clock/timestamp more consistent
2026-07-28 09:20:18 +08:00