answer

package
v1.801.455 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package answer is a researched answer to a hard question, with its sources cited.

It is the native answer engine: the bounded plan → search → read → rank → synthesize → cite → follow-up loop behind POST /v1/ask when a `mode` (search|news|research|deep) selects web grounding. It is a clean-room Hanzo implementation (NOT derived from any AGPL reference).

ONE HOME, ONE DOOR. This package is the loop's only home; /v1/ask is its only door. `mode` is a VALUE handed to that door — "deep research" is a mode, never a second route. The package registers no routes of its own: clients/ask owns the door and delegates web modes here.

THE SIX VALUES, one home each:

plan       → plan()        → []topic              (≤5 topics × 3–5 todos, best-effort)
search     → websearch.Search → []websearch.Result (in-process, keyless)
rank       → rank()        → []Source             (dedupe URL, host cap, relevance)
read       → read()        → []Source (enriched)  (the ONE crawl, apps/crawl)
survey     → survey()      → []Source             (search+read applied to a plan, ITERATED)
synthesize → synthesize()  → string               (streamed through the Sink)

BOUNDED. The fast modes make ≤3 LLM calls (1 plan + 1 synthesis + 1 follow-up) over one gathering pass. A survey adds ONE decision call per extra round (two on a round the model answers unreadably), itself bounded by mode.rounds (hard-capped at maxRounds), mode.deadline, mode.tokenCeiling, saturation, and the client still being connected. It is never an open agent loop.

GROUNDED, AND ONLY GROUNDED. Two properties hold against pages we did not author: the synthesis prompt fences every source with a per-request nonce, so a crawled page cannot print itself a source number the report then cites; and every markdown link in the answer is checked against the gathered set before it reaches the client, so a citation always points at a page THIS request fetched. Neither is a prompt instruction — a prompt is advice to a model, these are properties of the text that leaves the process. See ground.go.

ONE REVENUE DEBIT, NOT ONE DEBIT. Every answer debits the resolved payer once through the per-org ResourceMeter (Base.Bill): the mode's flat fee, which is the product price. That is the only REVENUE charge — but not the only charge. The AI plane this engine is handed is itself metered (build.go wraps it in meteredAIClient), so each internal completion also debits the payer per token against the same balance. Which layer should price /v1/ask is an open decision, recorded here rather than claimed away.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsMode

func IsMode(name string) bool

IsMode reports whether a request mode selects the answer engine. An empty or unknown mode is NOT an answer-engine request — /v1/ask's figure path handles it, so the advisor's existing behavior is untouched when no mode is set.

Types

type Engine

type Engine struct {
	cloud.Base
	AI    cloud.AIClient
	Model string
}

Engine is the answer engine value: the shared Base (logger + the ONE per-org meter) plus the AI plane it synthesizes with and the deployment's default model. The mounting package constructs it per request from what it already holds — the engine owns no state of its own.

func (Engine) Run

func (e Engine) Run(ctx context.Context, p Params, out Sink)

Run is the bounded loop, parameterized by mode — the ONE code path for search/news/research/deep. It emits the SearchEvent envelope through out, then meters the caller ONCE. A failed step degrades (fewer sources, snippets instead of pages, an honest note) rather than aborting the stream.

func (Engine) Serve

func (e Engine) Serve(c *zip.Ctx, in Request, q string) error

Serve answers one web-mode /v1/ask: resolve the billing subject → GATE the balance → run the bounded loop, streaming the SearchEvent envelope (SSE) or returning it as one JSON object. The caller is already gated as a validated principal by the door; here we additionally resolve the payer and gate spend BEFORE any work, so an out-of-funds caller gets a clean 402, never a half stream.

type Page

type Page struct {
	URL      string
	Markdown string
}

Page is the crawl seam's value: the URL a page was fetched for and its LLM-ready markdown. The answer engine depends on THIS, never on the crawler's own result shape — which is what lets a test inject a fake crawl.

type Params

type Params struct {
	// contains filtered or unexported fields
}

Params is the fully-owned per-request plan handed to Run(): safe to use after the Ctx is recycled (SSE) and retained by the async meter.

type Request

type Request struct {
	Mode       string   // search|news|research|deep
	Sources    []string // @hints appended to the web query: web,news,academic,github,reddit,x
	Model      string   // override the synthesis model
	Stream     *bool    // force SSE (else Accept: text/event-stream / ?stream=1)
	Language   string   // web-search language (BCP-47-ish)
	MaxSources int
	MaxQueries int
	FollowUps  *bool // default true
	System     string
}

Request is the answer engine's slice of the /v1/ask body. Every field is optional; mode selects the profile and the rest bound or override it.

type Sink

type Sink interface {
	// contains filtered or unexported methods
}

Sink receives the loop's envelope events — one method per SearchEvent variant, which keeps the loop declarative. Implemented by sseSink (streaming) and bufferSink (JSON).

There is no fail(): the loop never hard-fails. A down model degrades to an honest answer + a done frame (and is not billed), a down crawl degrades to snippets — so the client ALWAYS gets a terminal frame. The union's `error` variant is the CLIENT's (a transport failure it observes), never the server's. alive reports whether the client is still receiving. It is the loop's only window onto the socket: one research answer costs five minutes, three dozen page fetches and up to eight completions, and without this a browser tab closed a second in buys all of it. A sink with no socket (the JSON reply, a test) is always alive.

type Source

type Source struct {
	URL     string `json:"url"`
	Title   string `json:"title"`
	Snippet string `json:"snippet"`
	Engine  string `json:"engine,omitempty"`
	Favicon string `json:"favicon"`
	Text    string `json:"-"`
}

Source is one web source backing an answer — the @hanzo/ai SearchSource shape, field for field. It is the ONE source value in this package: search produces it, read() fills its Text, synthesis grounds on it, and the wire emits it verbatim in the `sources` and `done` frames.

SNIPPET IS WHAT THE CLIENT SHOWS; TEXT IS WHAT THE MODEL READS. Snippet is always the search engine's ~600-rune summary. Text is the fetched page — thousands of runes of markup we did not author, per source, re-ranked every round — and `json:"-"` is what keeps it off the wire: a rendered snippet is somebody else's text either way, but a bounded amount of it, and a survey that shipped its whole corpus in every snapshot would send a megabyte of duplicate SSE per answer. read() may touch no other field: the `sources` frame the client already rendered has to stay valid.

Jump to

Keyboard shortcuts

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