Skip to content

feat(agent): web_search and web_fetch — the agent can search the web and read pages - #119

Open
ndemianc wants to merge 3 commits into
developfrom
feat/web-search
Open

ndemianc wants to merge 3 commits into
developfrom
feat/web-search

Conversation

@ndemianc

Copy link
Copy Markdown
Contributor

The agent can search the web and read pages. Before this it had no web tool, and the browser edition's prompt said "There is no network" (the simulated shell's sentence), so a question about anything outside the workspace ended in "I don't have internet or web-search access".

Gateway side: systemu-net/thin.ly#462. Design note with the ChatGPT.app evidence: docs/WEB-SEARCH.md.

How ChatGPT.app does it

/Applications/ChatGPT.app (Codex desktop 26.930.31730) turns on OpenAI's hosted web_search:

  • Engine: the WebSearchMode setting is disabled, cached, indexed or live. The hosted tool's fields include external_web_access, filters.allowed_domains, user_location and search_context_size. Its actions are search {queries}, open_page and find_in_page.
  • Recorded dialog (in its thread database):
    • It searched twice, with 2 and then 3 queries per call.
    • Each search took 1.2–1.7 s.
    • The answer started at 17.5 s and streamed for 21 s.
    • Citations are Markdown links.

The time is the model's, not the search's.

What this adds

Two function tools, so every model can use them (extensions/levelcode-ai/webSearch.js):

Tool What it does
web_search({queries: 1-4, recency?}) The queries run at the same time. Results are merged (taking turns, de-duplicated, at most 12, each with an excerpt) and cached for 10 minutes. The model is told to cite with Markdown links.
web_fetch({url, find?}) Returns a page's readable text, or only the passages around find.

What web_fetch may open:

  • Only addresses it has seen. It opens an address only if it came from a search result, a page it opened, or the person's own message. A page cannot get the model to send data to an address it makes up.
  • Nothing local. On the desktop it never opens a page on a private or local network. Addresses are checked in the connection's own DNS lookup, IP literals before connecting, and every redirect again. In the browser, pages are read through LevelCode Cloud.

Where searches go:

  • LevelCode Cloud (thin.ly#462, credits, added to the run's cost) whenever the person is signed in.
  • Their own Brave Search, Tavily or Exa key on the desktop ("LevelCode: AI: Set Web Search Key…", SecretStorage).
  • Neither: the tools are not offered, and the model is told why and how to turn search on.

Settings and chat:

  • levelcode.ai.web.search and levelcode.ai.web.searchProvider are both application-scoped.
  • The browser manifest drops the own-key command and the provider setting.
  • In the chat: "Searched the web for … · N results · 0.3 s", with the found pages as links.

Tested (macOS, Node 24)

  • New tests: test/webSearch.test.js (22), webFetchNode.test.js (4, a real HTTP server), agentWebSearch.test.js (7, the real runAgent) and webChatRows.test.js (3). scripts/test-extensions.sh: 141 files.
  • Mutations: each of 21 one-edit mutations fails a test.
  • Real editor: scripts/web-search-editor-check.js runs the dev build with a stand-in model, a stand-in Brave and pages. The key is set from the command palette, the ChatGPT dialog runs end to end (with a fictional person), then search is turned off. 11/11.
  • Browser bundle: builds, with the network modules stubbed.
  • Types: tsc --checkJs shows no new errors in agent.js or extension.js.

Not run

A real search API or real model with these tools; the browser edition in a tab; Linux. The live comparison with ChatGPT on the same dialog needs a search key, entered by the owner.

Decisions (see the doc)

  • Search provider and key for LevelCode Cloud.
  • Price per query.
  • Billing for browser page reads.
  • On by default: the privacy page should mention that queries go to the provider.
  • Hosted search for Claude/GPT models.

…and read pages

Asked about something outside the workspace, the agent had no way to look it up, and the browser edition's
prompt said "There is no network" (the simulated shell's sentence), so it answered that it had no internet.

Modelled on ChatGPT.app, which turns on OpenAI's hosted web_search (docs/WEB-SEARCH.md has the evidence),
but as two ordinary function tools, so every model the agent runs on can use them:

- web_search({queries: 1-4, recency?}): the queries run at the same time; their lists are merged by taking
  turns and de-duplicated by address (at most 12 results, each with an excerpt), and the model is told to cite
  facts with Markdown links. A query answered in the last ten minutes comes from the cache.
- web_fetch({url, find?}): a page's readable text, or only the passages around `find` (ChatGPT's open_page and
  find_in_page). It opens only an address from a search result, a page it opened or the person's own message,
  so a page cannot get the model to send data to an address it makes up. On the desktop pages are read from
  this machine (webFetchNode.js), never one on a private or local network: checked in the connection's own
  DNS lookup, for IP literals before connecting, and again on every redirect. In the browser they are read
  through LevelCode Cloud.

Searches go to LevelCode Cloud (POST /ai/web/search, billed in credits, added to the run's cost) whenever the
person is signed in, or to their own Brave Search, Tavily or Exa key on the desktop ("LevelCode: AI: Set Web
Search Key…", SecretStorage). With neither, the tools are not offered and the model is told why and how to
turn search on. Settings levelcode.ai.web.search and levelcode.ai.web.searchProvider, both application-scoped;
the browser manifest drops the own-key command and provider setting, and its build stubs the Node network
modules. In the chat: "Searched the web for … · N results · 0.3 s" with the pages as links under it.

Tested: webSearch.test.js (22), webFetchNode.test.js (4, a real HTTP server on 127.0.0.1),
agentWebSearch.test.js (7, the real runAgent with scripted turns), webChatRows.test.js (3); host,
groupReducer, diagramHost and sessionExpiredHost harnesses follow the new pieces. 21 one-edit mutations each
fail a test. The fixtures use a fictional person and company.
scripts/web-search-editor-check.js runs the dev build with this extension against one local server standing in
for the model, Brave Search and two web pages. It sets the Brave key from the command palette as a person would,
asks the question the owner compared with ChatGPT.app (with a fictional person), and checks: both tools offered
and the web rules in the prompt; Brave asked both queries at the same time with the key; the results and the
page read back to the model, the page marked as data; the search row, its count and time and the found pages
as links in the chat; the answer's citation as a real link; with web search off, no tools and the reason told
to the model; nothing thrown in the chat page. Screenshots of the answer and of the opened activity group.

Ran on macOS: 11/11.
…ow, and the owner's decisions

The evidence from ChatGPT.app (the engine's hosted web_search modes and fields, the window's labels, and the
owner's recorded dialog with its timings: 1.2-1.7 s per search, the rest the model), the design of web_search /
web_fetch and the gateway endpoints, the billing hole closed in thin.ly, the same dialog side by side, what was
run and what was not (no real search API or real model yet), and five decisions for the owner.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 05:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants