researchroute

package
v1.0.0-beta.112 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package researchroute implements the route_search component from ADR-045 Phase 1 (PR 3 of six per docs/operations/22-adr045-phase1-plan.md).

route_search is the first LLM-judgment stage of the graph-search rule chain. It receives a publish trigger on component.route_search.<loop_id>, reads the upstream research.Intent + research.ClassifierOutput payloads from AGENT_LOOPS, builds a structured-emit prompt, calls the configured research_routing LLM endpoint, parses the model's JSON output into a research.RouteDecision, validates per-action arg shape, and writes the RouteDecision envelope plus a route.complete.<loop_id> trigger key that R2 (PR 6) watches to dispatch the next stage.

Architectural notes:

  • LLM-wrapping component, not agent-wrapping. Direct LLM call via the configured CapabilityResearchRouting endpoint; the chain does not delegate decision-making to a free-form ReAct loop.

  • Prompt is structured per discipline memory feedback_persona_prose_needs_decision_criteria: per-action purpose framing + decision-axis framing (no magic-number thresholds) + concrete examples for non-obvious choice points (decompose vs walk_seeds vs retighten) + negative-shape definition for synthesize_directly.

  • Args emitted by the model follow intent-not-structure (ADR-045 Amendment 2026-05-23): decompose carries axes/focus/ scope; walk_seeds carries seed references (name/partial_id/ candidate_index), not full 6-part entity IDs. Backend (execute_subqueries, PR 4) resolves to typed sub-queries and full IDs.

  • All public methods are safe for concurrent use across loops; the component holds no per-call mutable state. Same pattern as research-graph-classify.

Index

Constants

View Source
const (
	// DefaultRouteTimeout caps the structured-emit LLM call. Generous
	// enough for slower self-hosted endpoints; operators can tighten
	// for faster hosted models.
	DefaultRouteTimeout = 30 * time.Second

	// DefaultMaxResponseTokens caps the LLM's response budget. The
	// JSON-shaped output is small (action + args + rationale); 512 is
	// well above the worst case for the four action shapes.
	DefaultMaxResponseTokens = 512

	// DefaultMaxCandidatesInPrompt caps how many ClassifierOutput
	// candidates the prompt embeds. The router doesn't need the full
	// candidate list to decide — the top-N by relevance is enough —
	// and a tight cap keeps the prompt fitting frontier-tier and
	// small-model context windows alike.
	DefaultMaxCandidatesInPrompt = 10
)

Default knobs surfaced as exported constants so the prompt-builder tests and operator docs can reference them by name rather than duplicating literals.

View Source
const ComponentName = "research-graph-route"

ComponentName is the canonical registry name + log subsystem.

View Source
const SystemPromptMarker = "You are the routing stage of a graph-search pipeline"

SystemPromptMarker is the first sentence of buildSystemPrompt's output, exported so the e2e mock LLM scenario preset can match against it without copying the substring. A persona-prose edit that changes this sentence forces the importing test code to update too rather than silently failing the mock match (which would surface as "chain hung at route_search" with no link back to the marker drift).

Same pattern in processor/research-graph-assess/prompt.go and processor/research-graph-synthesize/prompt.go.

Variables

This section is empty.

Functions

func NewProcessor

func NewProcessor(rawConfig json.RawMessage, deps component.Dependencies) (component.Discoverable, error)

NewProcessor is the component-factory shape registered with the component registry. Parses + validates config, applies defaults, and constructs the Component with the injected production adapters. The LLM router is wired in Start() because the model registry isn't available at construction time.

func Register

func Register(registry *component.Registry) error

Register registers the route_search processor with the supplied component registry. Called from componentregistry.Register at process bootstrap so production binaries pick the component up without extra wiring.

Types

type Component

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

Component implements the route_search processor. Struct field set is intentionally small; lifecycle methods own the NATS / model- registry plumbing and the per-message handler hands off to the pure routeDecision function in handler.go.

func (*Component) ConfigSchema

func (c *Component) ConfigSchema() component.ConfigSchema

ConfigSchema implements Discoverable.

func (*Component) DataFlow

func (c *Component) DataFlow() component.FlowMetrics

DataFlow implements Discoverable.

func (*Component) Health

func (c *Component) Health() component.HealthStatus

Health implements Discoverable.

func (*Component) Initialize

func (c *Component) Initialize() error

Initialize is part of the LifecycleComponent contract. route_search has nothing to initialise pre-Start — bucket open + LLM client wire happen in Start so they can fail loudly and propagate.

func (*Component) InputPorts

func (c *Component) InputPorts() []component.Port

InputPorts implements Discoverable.

func (*Component) Meta

func (c *Component) Meta() component.Metadata

Meta implements Discoverable.

func (*Component) OutputPorts

func (c *Component) OutputPorts() []component.Port

OutputPorts implements Discoverable. route_search has no NATS- publishing output port: emits are KV writes to AGENT_LOOPS.

func (*Component) Start

func (c *Component) Start(ctx context.Context) error

Start opens the AGENT_LOOPS bucket, wires the LLM router from the model registry, subscribes to the configured input ports, and reports idle.

func (*Component) Stop

func (c *Component) Stop(timeout time.Duration) error

Stop drains subscriptions, closes the LLM client, and flips `started` under c.mu so a concurrent Health / DataFlow read can't see a torn read of the lifecycle flag.

type Config

type Config struct {
	Ports *component.PortConfig `` /* 167-byte string literal not displayed */

	LoopsBucket string `` /* 175-byte string literal not displayed */

	RouteTimeout time.Duration `` /* 212-byte string literal not displayed */

	MaxResponseTokens int `` /* 222-byte string literal not displayed */

	MaxCandidatesInPrompt int `` /* 286-byte string literal not displayed */
}

Config holds operator-tunable knobs for the route_search component.

LLM wiring follows the same model-registry capability seam as processor/research-graph-classify: operator declares CapabilityResearchRouting in the model registry; the component resolves it at Start() time. Absence is a startup error — unlike the optional LLM tier on nl_classify, route_search has no keyword-only fallback (its entire job is the LLM judgment).

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a default Config skeleton with the standard route_search input port.

func (*Config) ApplyDefaults

func (c *Config) ApplyDefaults()

ApplyDefaults fills in defaults for unset fields.

func (*Config) Validate

func (c *Config) Validate() error

Validate validates the configuration. Negative caps are rejected; zero values fall through to ApplyDefaults (treated as "use default").

type LoopStore

type LoopStore interface {
	// GetIntent loads the research_intent payload from the
	// research.requested.<loopID> key. Returned for the prompt's
	// topic + hints context.
	GetIntent(ctx context.Context, loopID string) (*research.Intent, error)

	// GetClassifierOutput loads the upstream ClassifierOutput from
	// the classify.complete.<loopID> trigger key. The router needs
	// the candidate set, hints, and confidence to make its decision.
	GetClassifierOutput(ctx context.Context, loopID string) (*research.ClassifierOutput, error)

	// PutRouteDecision writes the RouteDecision envelope at R2's
	// trigger key route.complete.<loopID>.
	PutRouteDecision(ctx context.Context, loopID string, envelope []byte) error

	// PutSnapshot writes the envelope at a stable non-trigger key
	// route.snapshot.<loopID> so operators / downstream queryability
	// can read the full decision without racing R2's wildcard
	// watcher. Same pattern as research-graph-classify.
	PutSnapshot(ctx context.Context, loopID string, envelope []byte) error
}

LoopStore is the AGENT_LOOPS read/write surface this component consumes. Production wraps natsclient.KVStore; tests substitute an in-memory map.

type Router

type Router interface {
	Route(ctx context.Context, systemPrompt, userPrompt string, maxResponseTokens int) (content string, reason string, err error)
}

Router is the narrow LLM surface this component consumes. Production satisfies it via llmRouterAdapter wrapping a real graph/llm.Client; tests substitute a fake that returns a deterministic raw JSON response per scenario without standing up any LLM infrastructure.

Route returns the raw response Content + a short reason string the adapter can use for error diagnostics (model identifier, finish reason, etc.). The handler parses the Content as JSON into a RouteDecision and validates separately so prompt iteration and response-shape drift surface as separate test failures.

Jump to

Keyboard shortcuts

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