search

package
v0.2.1-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package search is the web-search layer the v3 session agent calls through: one Provider that answers a query with a list of links, one Fetcher that turns a link into readable text, and an OPEN registry that decides which implementation of each the surface actually gets.

The registry is the design. Exa is one plug and not the point — it is the keyed plug that happens to exist today. A search back end that arrives later declares itself from its own file's init, and no caller changes. The one deliberate list in this file orders zero-key defaults: without it, Go's filename-ordered inits would silently decide which free service wins.

The other half of the design is that search WORKS with no configuration at all. A person who has typed no keys still gets results, because the last rung of the ladder is a zero-key plug (Firecrawl's keyless endpoint) that is always available. Keys are an upgrade someone opts into when they care about result quality or rate ceilings, not a prerequisite for the agent to look anything up. DuckDuckGo remains registered as a safety-valve pin.

The package imports nothing of the surface — no session, no config, no provider, no store. Configuration arrives as a plain Options value, so the resolution law can be tested without a profile directory on disk and the wiring wave can fill Options from whichever settings store it likes.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoAPIKey = errors.New("no API key")

ErrNoAPIKey is shared by keyed plugs so the tool, settings hint, and tests all describe an unavailable pinned search with the same words.

Functions

func Failure

func Failure(name string, err error) string

Failure spells a search failure once for the tool and for settings guidance that quotes the exact answer a pinned, unconfigured plug will produce.

func FetchWithName

func FetchWithName(ctx context.Context, fetcher Fetcher, url string) (string, string, error)

FetchWithName is SearchWithName for page reads. Its name is used only on a failed receipt today, but it must still describe the fetcher that actually ran when settings change while a request is in flight.

func Live

func Live(options func() Options) (Provider, Fetcher)

Live returns a provider and fetcher that resolve again for every operation. The first resolution decides whether each capability exists at all; built binaries always have both, while an intentionally empty registry still leaves the capability off the session's belt. The wrappers retain only the options function, so a copy inherited by child work observes the same settings as its parent.

func RegisterFetch

func RegisterFetch(f Fetcher)

RegisterFetch adds a fetch plug, on the same terms as RegisterSearch.

func RegisterSearch

func RegisterSearch(p Provider)

RegisterSearch adds a search plug. Meant to be called from a package's init, which is why it panics rather than returning an error: a plug that failed to register would not fail at registration but silently at resolution, by resolving to something else that looked fine.

Order of registration is recorded but deliberately does NOT decide the auto winner between a keyed and a zero-key plug — see Resolve. Go runs a package's init functions in filename order, and a law that depended on that would be a law that changed when a file was renamed.

func RenderFetch

func RenderFetch(text string) string

RenderFetch formats fetched page text for the agent, capped, with the overflow announced rather than hidden. A model that can see it was cut can ask for the rest or narrow its question; a model handed a silently truncated page concludes the page ended there.

func RenderResults

func RenderResults(results []Result, limit int, backend string) string

RenderResults formats results for the agent: a compact numbered list of "title — url" with the snippet indented under it, and a count footer so the model can tell "these are all of them" from "these are the first few". The footer also names the plug that answered so both the model transcript and the compact tool receipt can report the same fact.

limit is the caller's own cap, further clamped to [maxRendered].

func Resolve

func Resolve(opts Options) (Provider, Fetcher)

Resolve answers which plugs a session's searches and fetches go to.

The law, in order:

  1. A PIN WINS. opts.Provider naming a registered plug ends it, whether or not that plug has its key. An explicit instruction is honoured, and the resulting error names the missing key plainly — which is a better outcome than silently searching somewhere the person did not ask for. A pin naming nothing registered falls through rather than failing, so a stale settings value cannot take search away.
  2. Otherwise the first AVAILABLE KEYED plug in registration order. Keyed means the plug reports itself unavailable under empty Options — exa, when ExaKey is set.
  3. Otherwise the zero-key default: the earliest named plug in [keylessOrder], then unlisted plugs in registration order. That is Firecrawl for search and jina for fetch. This rung cannot fail to produce a plug, which is why Resolve returns no error.

Availability is key presence and nothing more, so the whole ladder is a few string comparisons and can run per call.

The two registries resolve independently — pinning "ddg" for search still leaves fetch free to pick exa-fetch when a key is present — because the two jobs are different jobs and the best plug for one is not the best for the other.

Resolve returns nil for a side whose registry is empty. That does not happen in a built binary, where the zero-key plugs self-register, but it is what a test that empties the registry sees.

func ResultSummary

func ResultSummary(rendered string) string

ResultSummary returns the named count footer from rendered search output. It accepts only the shapes RenderResults emits, which keeps an error or an arbitrary final body line from becoming a misleading success receipt.

func Status

func Status(opts Options) string

Status describes the search plug the same options would select now. It is intentionally resolved from Options rather than from saved session state, because both the status deck and the next call must move together.

Types

type FetchPlug

type FetchPlug interface {
	Fetcher
	Available(opts Options) bool
	Bind(opts Options) Fetcher
}

FetchPlug is SearchPlug for the fetch side.

type Fetcher

type Fetcher interface {
	// Name is the plug's identifier, as on [Provider].
	Name() string
	// Fetch returns the page at url as text.
	Fetch(ctx context.Context, url string) (string, error)
}

Fetcher turns one page into clean text — the second half of a search: the agent finds a link, then reads it. Markdown-ish output is expected and welcome; raw HTML is not.

func RegisteredFetch

func RegisteredFetch() []Fetcher

RegisteredFetch lists the fetch plugs in registration order.

type Options

type Options struct {
	// Provider pins one plug by name. Empty means auto — the resolution law
	// in [Resolve] picks. The pin is matched against both registries, so
	// naming a fetch plug pins the fetcher and leaves search on auto.
	Provider string
	// ExaKey is the Exa API key. Its presence is what makes the exa plugs
	// available.
	ExaKey string
	// FirecrawlKey is optional: Firecrawl search is keyless, and a key raises
	// its ceiling rather than gating availability. The fetch plug does require
	// it, so a paid account upgrades page reads while search keeps working with
	// or without one.
	FirecrawlKey string
	// JinaKey is optional: r.jina.ai answers unauthenticated at 20 requests
	// per minute, and a key only raises that ceiling. So it does NOT gate
	// availability — jina is a zero-key plug that happens to take a key.
	JinaKey string
	// HTTPClient is the client every plug makes its requests with. Nil means
	// the package default. A caller that wants a proxy, a custom transport
	// or a recording client in a test sets it here.
	HTTPClient *http.Client
}

Options is everything the layer is configured with. A plain struct, filled by the caller: a sibling package owns settings, and this package must not learn its shape.

type Provider

type Provider interface {
	// Name is the plug's identifier — the string a person pins in settings
	// and the name that appears in an error.
	Name() string
	// Search answers a query with at most limit results. A limit of zero or
	// less means "the plug's own sensible default".
	Search(ctx context.Context, query string, limit int) ([]Result, error)
}

Provider is a search back end.

func RegisteredSearch

func RegisteredSearch() []Provider

RegisteredSearch lists the search plugs in registration order, and RegisteredFetch the fetch plugs — for a settings screen that wants to show what exists rather than what won.

type Result

type Result struct {
	// Title is the page title, tags stripped.
	Title string
	// URL is the destination, already unwrapped from any redirect the back
	// end put in front of it.
	URL string
	// Snippet is the back end's extract: the sentences that made it a hit.
	Snippet string
	// Published is the publication date as the back end reported it, in
	// whatever format that was, and empty when it reported none. It is
	// carried as a string rather than a time.Time on purpose: back ends
	// disagree about format and precision, half of them guess, and the only
	// consumer is a renderer that shows it to a model. Parsing it would
	// invent certainty the source does not have.
	Published string
}

Result is one hit: what every back end has in common after mapping. The fields are deliberately flat strings — this is what gets rendered into a prompt, not a model of anybody's API.

func SearchWithName

func SearchWithName(ctx context.Context, provider Provider, query string, limit int) ([]Result, string, error)

SearchWithName runs one search and returns the plug chosen for that same operation. Live providers implement the private method so resolving current settings and naming the receipt share one snapshot; an ordinary Provider is already one fixed plug and needs no extra machinery on its public interface.

type SearchPlug

type SearchPlug interface {
	Provider
	// Available reports whether opts carry what this plug needs. It must be
	// cheap and LOCAL — key presence and nothing else. Never a network
	// probe: resolution happens on every call, and a resolution that can
	// hang or cost money is a resolution that will do both at the worst
	// moment.
	Available(opts Options) bool
	// Bind returns the plug configured for opts. The prototype is not
	// mutated, so one registration serves every session in the process.
	Bind(opts Options) Provider
}

SearchPlug is the optional half a Provider implements when it needs configuration to run. The registry stores prototypes — a plug registers itself once from an init, before any key is known — and hands each prototype the Options at resolve time to produce the configured provider.

A registered Provider that does NOT implement SearchPlug is legal: it is treated as always available, needing nothing, and is used as-is.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL