Skip to main content

Web search

A bound agent answers from your workspace. Some questions need the world outside it — a rule that changed last month, a price, a public filing. This page is how you give an agent the live web, and how to trust what comes back.

The hard part is not fetching. It is knowing what you got: a page the system actually read, a snippet a search engine showed it, or a model's prose about a page it never opened. Libra OS labels all three, and the labels are the point.

Turn it on

Web access is a capability an agent declares. Nothing reaches the internet unless an agent is given a tool that does.

capabilities:
- search_web # search, with optional page content
- fetch_url # read one specific URL
- research_query # high-accuracy lookup for a single question
- deep_research # multi-aspect research with synthesis

Give an agent the least of these that does its job. search_web and fetch_url cover most work. deep_research plans several angles, runs them in parallel and synthesizes — powerful, and the most expensive thing on this page.

Configuration

There is nothing to configure.

Search and page-fetch run on the same gateway as your model tiers, so they reuse the credential you already set for chat. If OPENAI_API_KEY and OPENAI_API_BASE are working, web search works.

That is deliberate. It used to take a second variable naming the same credential for the same host — and a deployment could have chat working perfectly while web search silently did nothing, because nobody knew the second variable existed.

Bringing your own provider. Set your own search keys and they are used instead, as a fallback chain behind the gateway; with no gateway configured they become the primary. Pin one explicitly with LIBRA_OS_WEB_SEARCH. Page fetching is chosen separately with LIBRA_OS_WEB_FETCHER — the default is fine unless you self-host a crawler.

What a result actually is

Every result declares what its text is and what happened to it. Read these two fields before you trust a quote.

representationMeaning
search_snippetThe snippet a search engine returned
provider_excerptPage text the provider's crawler fetched
page_extractPage text Libra OS itself fetched
provider_summaryThe provider's generated prose about the page
model_answerA model's answer to your query — not a source at all

The first three are page-derived: they are words that appear on the page. The last two are model-generated. A provider_summary can be accurate and still be the wrong thing to quote, because nobody guarantees the page says it.

statusMeaning
openedThe page was read
(empty)The result was found and nothing more was attempted
blocked_policyWe declined to read it — publisher policy or robots.txt
blocked_softThe site served an anti-bot challenge instead of the page
blocked_targetThe URL was refused by the SSRF guard
fetch_failedA fetch was attempted and returned nothing

Found and opened are different claims. An answer citing ten results where none has status: opened was assembled from snippets.

A model answer never sits among your results. It travels separately as provider_answer, so it cannot be mistaken for a hit.

Bound the spend

Research is the one capability that can run away with a turn. Two per-call controls:

{
"metadata": {
"max_searches": 3, // hard cap on searches this turn
"research_query": "..." // the question to research
}
}

max_searches is a ceiling across every search the turn makes — the planner's fan-out, the tools, enrichment. Without it a multi-aspect plan can issue several searches where one would do.

research_query matters more than it looks. Applications usually send an assembled message: a response-language directive, formatting rules, conversation history, then the user's actual question. Left alone, the planner decomposes all of it, and fragments of your own system prompt can end up as search queries at a third-party provider. Passing the question as its own field prevents that. Libra OS also refuses to send assembled-prompt text as a query, but the explicit field is better than the guard.

Server-side budgets exist for the long paths — LIBRA_OS_RESEARCH_SKILL_TIMEOUT, LIBRA_OS_DEEP_RESEARCH_TIMEOUT, LIBRA_OS_DEEP_SEARCH_MAX_CONCURRENCY — so one turn cannot occupy the deployment.

Reading the answer

Every chat response carries a grounding verdict. For web work these are the ones to watch:

VerdictWhat happened
groundedEvidence was retrieved and used
unopened_sourcesIt searched, cited what it found, and opened none of it
unsupported_claimIt claimed a source it never consulted
ungrounded_no_chunksNothing was retrieved and no tool ran

unopened_sources is the one worth alerting on. The answer is confident, the citations look real, and every one is a search snippet — which is not a lie, but is much weaker than it appears. It usually means fetches were blocked or failing, and it is the single most useful signal that web research has silently degraded.

When a source refuses

Not every page can be read, and Libra OS reports why rather than failing generically:

  • blocked_policy — the publisher's robots.txt disallows this path, or the deployment's source policy marks it licensed or manual-only. This is a lawful refusal, not an error: it is not retried and it does not count as a failure in health metrics.
  • blocked_soft — the site returned an anti-bot challenge with a 200 status. The refusal names the vendor where it can be identified.

Libra OS does not evade these. No fingerprint spoofing, no CAPTCHA solving, no proxy rotation. A wall is reported as a wall — a refusal you can see beats content you cannot trust.

A worked example

curl -s -X POST $KERNEL/v1/messages \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{
"model": "research-assistant",
"max_tokens": 900,
"metadata": { "max_searches": 3, "research_query": "What changed in the 2026 filing deadline?" },
"messages": [{"role": "user", "content": "What changed in the 2026 filing deadline? Cite sources."}]
}'

Check three things in the response: grounding is grounded, at least one cited source has status: opened, and any text you plan to quote is page-derived rather than provider_summary.

Where to go next