research

package
v1.0.0-beta.94 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package research defines the payload types for the ADR-045 graph search rule chain: Intent, SearchResult, and RouteDecision.

These are the wire shapes the chain's coordinated components and the continuation rule pass through the payload registry. Phase 1 PR 1 lands the registry surface so PRs 2-6 can land components and the rule chain without churn on the schema contract.

See docs/adr/045-graph-search-rule-chain.md for the architecture and docs/operations/22-adr045-phase1-plan.md for the PR sequence.

Index

Constants

View Source
const (
	// Domain is the payload registry domain for the ADR-045 graph
	// search rule chain.
	Domain = "research"

	// SchemaVersion is the current schema version for research payloads.
	SchemaVersion = "v1"
)

Domain and version constants for the research message namespace. All three payload types (ResearchIntent, SearchResult, RouteDecision) share the domain and version; only the category differs.

View Source
const (
	// CategoryIntent is the caller's research request. Topic + free-form
	// hints + budget. Entity IDs and predicates are outputs of the chain,
	// not inputs (see ADR-045's classifier-first amendment).
	CategoryIntent = "intent"

	// CategoryResult is the chain's terminal output. Synthesis text plus
	// evidence refs the parent can quote back, plus decomp trace for
	// trajectory review.
	CategoryResult = "result"

	// CategoryRouteDecision is route_search's structured emit: one of
	// four actions plus action-specific args plus rationale.
	CategoryRouteDecision = "route_decision"

	// CategoryClassifierOutput is the nl_classify component's emit:
	// classifier hints + initial candidate set. R1 fires on the
	// matching trigger key; route_search (PR 3) consumes the payload
	// to pick a routing action.
	CategoryClassifierOutput = "classifier_output"

	// CategoryExecutionOutput is the execute_subqueries component's
	// emit (ADR-045 Phase 1 PR 4): dedup'd + ranked + budget-
	// enforced evidence array + provenance. R3 fires on the matching
	// trigger key; assess_sufficiency (PR 5) consumes the payload
	// to pick sufficient / refine.
	CategoryExecutionOutput = "execution_output"

	// CategoryAssessmentOutput is the assess_sufficiency component's
	// emit (ADR-045 Phase 1 PR 5): the sufficient/refine decision
	// over the upstream ExecutionOutput. R3 dispatches synthesize on
	// Sufficient, refine on !Sufficient.
	CategoryAssessmentOutput = "assessment_output"
)

Category constants for research payload types.

View Source
const (
	// ActionSynthesizeDirectly short-circuits to synthesize_answer when
	// classifier output is already enough to answer the topic. Many
	// parent topics will land here; the chain doesn't pay for multi-hop
	// work it doesn't need.
	ActionSynthesizeDirectly = "synthesize_directly"

	// ActionRetighten re-runs nl_classify with refined topic/hints when
	// the initial classification produced noisy or empty hits. Bounded
	// at MaxIterations=2 on R2's retighten branch.
	ActionRetighten = "retighten"

	// ActionWalkSeeds dispatches execute_subqueries with seed
	// REFERENCES (names, partial IDs, or candidate-list indices) the
	// classifier surfaced. Per ADR-045 Amendment 2026-05-23
	// (intent-not-structure), the model emits references; the backend
	// resolves to full 6-part entity IDs via the entity index. Multi-
	// hop expansion from concrete seeds — structurally different from
	// topic-wide decompose. See WalkSeedsArgs.
	ActionWalkSeeds = "walk_seeds"

	// ActionDecompose dispatches execute_subqueries with the
	// decomposition INTENT (axes, focus, scope). Per ADR-045
	// Amendment 2026-05-23 (intent-not-structure), the model emits
	// intent; the backend constructs the typed sub-queries from
	// templates. The v1 default-path now used only when the
	// classifier shows it's necessary. See DecomposeArgs.
	ActionDecompose = "decompose"
)

RouteAction values for RouteDecision.Action. Closed enum per ADR-045 section "Why this is not just-a-better-classifier" — the four actions are the load-bearing taxonomy. Validate rejects values outside this set so structured-emit retries can trigger per ADR-035.

View Source
const (
	// PredicateResearchRequested marks a research-pipeline loop entity
	// as the target of a research_graph tool invocation. Triple
	// subject = research-loop entity ID; object = the topic string.
	// R0 of the rule chain watches for this triple to kick off the
	// chain.
	PredicateResearchRequested = "research.requested"

	// PredicateResearchTopic carries the topic verbatim. Distinct from
	// PredicateResearchRequested because the latter doubles as the
	// chain-kickoff trigger; this predicate is the durable record of
	// what the parent actually asked for.
	PredicateResearchTopic = "research.topic"

	// PredicateResearchHint stamps a single hint key/value pair. One
	// triple per hint, with the key encoded in the Object field as
	// "<key>=<value>". Multi-triple is preferred over JSON-stringified
	// hint maps so graph queries can filter on specific hint keys.
	PredicateResearchHint = "research.hint"

	// PredicateResearchBudgetTokens carries the resolved per-call token
	// budget (after defaulting). Stored as string per Triple.Object
	// shape; consumers parse to int.
	PredicateResearchBudgetTokens = "research.budget_tokens"

	// PredicateResearchMaxIterations carries the resolved refine-loop
	// cap (after defaulting).
	PredicateResearchMaxIterations = "research.max_iterations"

	// PredicateResearchParentLoop links the research-pipeline loop
	// back to its parent loop entity ID. Stamped by the research_graph
	// tool from call.LoopID so the continuation rule can route the
	// SearchResult back to the right caller.
	PredicateResearchParentLoop = "research.parent_loop"

	// PredicateLoopRole stamps the loop entity's role. Same convention
	// as other agentic loops; lets ops dashboards filter
	// research-pipeline loops without parsing intent payloads.
	PredicateLoopRole = "loop.role"
)

Predicate constants for triples emitted by the research_graph chain. Every predicate lives under the research.* namespace so graph queries can filter chain triples cleanly without colliding with other agentic systems.

Triple emission discipline (ADR-028):

  • Bulky payloads (synthesis text, evidence body) live in ObjectStore via ContentStorable; triples carry the ref.
  • LLM-authored predicates (eventual route_rationale and synthesis predicates introduced in PR 3 and PR 5) MUST default to WithRuleOpaque(true) per feedback_llm_authored_predicates_rule_opaque — rules branch on typed fields, not free-form text. PR 1 reserves the namespace here; the named constants land with the PRs that emit them so the contract is reviewable alongside the emission.
View Source
const (
	SeedRefTypeName           = "name"
	SeedRefTypePartialID      = "partial_id"
	SeedRefTypeCandidateIndex = "candidate_index"
)

SeedRefType constants for WalkSeedsArgs.Seeds[*].RefType. Closed enum enforced by ParseWalkSeedsArgs. The backend (execute_subqueries) interprets the ref string differently per type: a "name" matches the entity's display name, a "partial_id" matches any suffix of the 6-part federated ID, a "candidate_index" indexes into the upstream ClassifierOutput candidate list.

View Source
const (
	DecomposeScopeNarrow = "narrow"
	DecomposeScopeMedium = "medium"
	DecomposeScopeBroad  = "broad"
)

DecomposeScope constants for DecomposeArgs.Scope. Closed enum; empty resolves to ScopeMedium at the backend so a model that omits the field still routes deterministically.

View Source
const DefaultBudgetTokens = 4000

DefaultBudgetTokens is ResearchIntent.BudgetTokens default per docs/operations/22-adr045-phase1-plan.md PR 1 spec.

View Source
const DefaultMaxIterations = 5

DefaultMaxIterations is ResearchIntent.MaxIterations default. The refine loop cap on R4 ships with the same default — see ADR-045 rule chain R4.

View Source
const PipelineRole = "research_pipeline"

PipelineRole is the role string carried on the research-pipeline LoopEntity in AGENT_LOOPS. Lets downstream tooling (trajectory viewers, ops dashboards) filter research loops from coordinator and task loops without parsing intent payloads.

View Source
const TripleSource = "agent-research-graph"

TripleSource is the Source field on triples emitted by the research_graph tool. Mirrors the decide / write_todos convention so operators can distinguish chain-kickoff triples from later component emissions in graph queries.

Variables

This section is empty.

Functions

func IsValidDecomposeScope

func IsValidDecomposeScope(s string) bool

IsValidDecomposeScope reports whether s is one of the closed decompose-scope values. Empty string is treated as valid (it resolves to "medium" at the backend); callers that want to reject empty should test before calling.

func IsValidRouteAction

func IsValidRouteAction(s string) bool

IsValidRouteAction reports whether s is one of the four canonical router actions. Exported so route_search's structured-emit validator can reuse the closed set.

func IsValidSeedRefType

func IsValidSeedRefType(s string) bool

IsValidSeedRefType reports whether s is one of the closed seed reference types. Exported so component validators can reuse the set.

func RegisterPayloads

func RegisterPayloads(reg *payloadregistry.Registry) error

RegisterPayloads registers the three research payload types with the supplied registry. Called from payloadbuiltins.Register at process bootstrap so every production binary picks up the types without extra wiring.

Mirrors the agentic.RegisterPayloads + agentic/operating-model shape so the bootstrap aggregator can call this uniformly.

Types

type AssessmentOutput

type AssessmentOutput struct {
	// Topic echoes the originating Intent topic so downstream
	// consumers and trajectory viewers don't have to re-fetch the
	// intent payload for logging / display. Required.
	Topic string `json:"topic"`

	// Sufficient is the load-bearing decision. True means the
	// evidence array is enough to synthesize a useful answer; R3 routes
	// to synthesize. False means the chain should refine; R4 routes
	// the RefinedQueries through another execute_subqueries pass.
	Sufficient bool `json:"sufficient"`

	// RefinedQueries is the refine path's payload — short natural-
	// language sub-query strings the assessor believes would close the
	// gap. Empty when Sufficient is true. The Phase 1 refine path
	// concatenates these into a single hint set and re-runs nl_classify;
	// Phase 2 may dispatch them as parallel typed sub-queries.
	//
	// The shape is deliberately stringy (not typed sub-queries) per
	// feedback_tool_signature_intent_not_structure: the assessor model
	// expresses gap intent, the backend constructs typed dispatches.
	RefinedQueries []string `json:"refined_queries,omitempty"`

	// Rationale is the assessor's natural-language justification for
	// the decision. Captured for operator trajectory review; rule-
	// opaque per discipline memory.
	Rationale string `json:"rationale,omitempty"`

	// Confidence is the assessor's confidence in the sufficiency
	// decision (0..1). Optional — the assess prompt asks for it as
	// observability; rules do not branch on it in Phase 1. Operator
	// trajectory review uses it to spot prompts that flip flags
	// with low conviction.
	Confidence float64 `json:"confidence,omitempty"`

	// EvidenceCount echoes len(ExecutionOutput.Evidence) the assessor
	// saw. Observability — lets the operator confirm the assessor
	// actually had evidence to read when it claimed Sufficient (a
	// hallucinated Sufficient=true on EvidenceCount=0 is a prompt
	// bug worth catching).
	EvidenceCount int `json:"evidence_count,omitempty"`

	// Degraded flags an assessment that couldn't run cleanly — e.g.
	// the LLM call failed and the component fell back to a
	// "Sufficient=false, refine with original intent" safety route,
	// or upstream ExecutionOutput was missing. assess_sufficiency
	// surfaces this so R3 / R4 can downgrade their next-stage
	// confidence without stranding the chain on a hard abort.
	Degraded       bool   `json:"degraded,omitempty"`
	DegradedReason string `json:"degraded_reason,omitempty"`
}

AssessmentOutput is the assess_sufficiency component's emit (ADR-045 Phase 1 PR 5): the sufficient/refine decision over the upstream ExecutionOutput's evidence array. Phase 1's R3 rule dispatches synthesize_answer when Sufficient is true and the refine loop on R4 when Sufficient is false.

Per feedback_llm_authored_predicates_rule_opaque, when this payload is stamped onto loop entity triples the Rationale field's triple-predicate is published with WithRuleOpaque(true). Rules branch on the typed Sufficient boolean only.

func (*AssessmentOutput) MarshalJSON

func (p *AssessmentOutput) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler with the alias-recursion guard.

func (*AssessmentOutput) Schema

func (p *AssessmentOutput) Schema() message.Type

Schema implements message.Payload.

func (*AssessmentOutput) UnmarshalJSON

func (p *AssessmentOutput) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler with the alias-recursion guard.

func (*AssessmentOutput) Validate

func (p *AssessmentOutput) Validate() error

Validate implements message.Payload. Required: Topic. RefinedQueries items are checked for non-empty strings when present (an empty query string is an assessor-prompt bug). When Sufficient is true RefinedQueries SHOULD be empty — the assessor shouldn't refine what it just declared sufficient — but the check is a Warn at the consuming component, not a Validate-time reject, so the chain stays moving on imperfect assessor emits.

type Candidate

type Candidate struct {
	// EntityID is the matching graph entity. Required.
	EntityID string `json:"entity_id"`

	// Label is a human-readable name for the entity. Optional; empty
	// when the candidate source doesn't carry a label (some graph
	// queries return only IDs).
	Label string `json:"label,omitempty"`

	// Type is the entity's domain type when known (e.g. "drone",
	// "sensor"). Optional.
	Type string `json:"type,omitempty"`

	// Relevance is the within-tier ranking score (higher = better).
	// Per ADR-045 Phase 1, cross-tier comparison isn't meaningful —
	// PR 4 keeps per-tier ordering + recency tie-break; Phase 2 may
	// add a learned ranker.
	Relevance float64 `json:"relevance,omitempty"`

	// SnippetText is a short inline preview useful for prompt
	// injection. Omit when the candidate has no readable preview.
	SnippetText string `json:"snippet_text,omitempty"`

	// Tier is the retrieval tier that surfaced this hit. Same
	// taxonomy as Evidence.Tier in result.go so the evidence schema
	// stays consistent across PR 2 (this file) and PR 4.
	Tier string `json:"tier"`

	// Source is the retrieval source within the tier — e.g.
	// "classifier_chain", "search_graph". Required so PR 4 can
	// classify candidates by retrieval pathway.
	Source string `json:"source"`
}

Candidate is one entity hit the classifier surfaced for the topic. Mirrors the search_graph EntityDigest shape (entity_id + label + type + relevance score + optional snippet) so PR 4 (execute_subqueries) can consume nl_classify's output and search- graph's output through one evidence schema.

Provenance is mandatory: every Candidate carries the Tier ("0" keyword / "1" embedding/BM25 / "2" neural — Phase 2) and Source (the retrieval method, e.g. "classifier_chain", "search_graph") so downstream rules can filter by retrieval pathway.

func (*Candidate) Validate

func (c *Candidate) Validate() error

Validate checks the required fields and the closed Tier enum.

type ClassifierOutput

type ClassifierOutput struct {
	// Topic is the topic the classifier ran on. Echoed so the
	// downstream consumer (route_search) doesn't have to read the
	// intent payload separately.
	Topic string `json:"topic"`

	// Tier is which classifier tier produced the hint set:
	// "0" (keyword), "1" (embedding/BM25), or "3" (LLM). The
	// numbering follows ClassifierChain conventions (T0/T1/T2/T3 — T2
	// neural is Phase 2, not currently emitted). Empty/"0" is the
	// default fall-through when no tier matched.
	//
	// IMPORTANT: this is the *classifier* tier and shares no enum
	// with [Candidate.Tier] / [Evidence.Tier], which are *retrieval*
	// tiers ("0"/"1"/"2"). A LLM-tier classifier may surface
	// Tier="3" here while its surfaced candidates carry Tier="0" or
	// "1". Don't copy this string into a Candidate without
	// re-mapping — Candidate.Validate rejects "3".
	Tier string `json:"tier,omitempty"`

	// Intent is the LLM-or-embedding-detected intent classification.
	// Empty when the keyword tier matched (KeywordClassifier doesn't
	// set Intent — its semantics live in the Options map).
	Intent string `json:"intent,omitempty"`

	// Confidence is the classifier's confidence in the hint set.
	// 1.0 for keyword matches, <=1.0 from embedding similarity.
	Confidence float64 `json:"confidence,omitempty"`

	// Hints is the operator-facing flattened classifier output: time
	// range, geo bounds, path intent, aggregation type, etc. Wire
	// shape matches ClassificationResult.Options so consumers can
	// reconstruct a SearchOptions when they need to re-execute.
	Hints map[string]any `json:"hints,omitempty"`

	// Candidates is the initial candidate set: entities the
	// classifier surfaced, with scores + snippets where available.
	// Empty when the topic produced no hits — PR 3's route_search
	// will choose "retighten" or "decompose" in that case.
	Candidates []Candidate `json:"candidates,omitempty"`

	// Degraded is set when the candidate retrieval fell back to a
	// less-rich path (e.g. server-side semantic fallback). Carried
	// through so PR 3's router can treat the candidates as
	// advisory.
	Degraded bool `json:"degraded,omitempty"`

	// DegradedReason is a short operator-readable label describing
	// the fallback path when Degraded is true. Empty otherwise.
	DegradedReason string `json:"degraded_reason,omitempty"`
}

ClassifierOutput is the nl_classify component's emit: the classifier hints (extracted from ClassificationResult.Options) plus the initial candidate set surfaced by executing the resulting SearchOptions against the graph. R1 fires on this payload (via the classify.complete.<loop_id> trigger key) and route_search (PR 3) consumes it to choose one of the four routing actions.

func (*ClassifierOutput) MarshalJSON

func (p *ClassifierOutput) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler with the alias-recursion guard.

func (*ClassifierOutput) Schema

func (p *ClassifierOutput) Schema() message.Type

Schema implements message.Payload.

func (*ClassifierOutput) UnmarshalJSON

func (p *ClassifierOutput) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (*ClassifierOutput) Validate

func (p *ClassifierOutput) Validate() error

Validate implements message.Payload. Topic is required; Candidates are individually validated when present.

type DecompTrace

type DecompTrace struct {
	// RouterAction is the value emitted by route_search at the chain's
	// first LLM judgment. One of the four ActionXxx constants.
	RouterAction string `json:"router_action,omitempty"`

	// SubQueries records the decomposed sub-queries the router
	// generated when RouterAction == ActionDecompose. Free-form per
	// Phase 1; typed sub-query schemas land with PR 3's router action
	// arg schemas.
	SubQueries []map[string]any `json:"sub_queries,omitempty"`

	// SeedEntities records the walk_seeds entity set when
	// RouterAction == ActionWalkSeeds.
	SeedEntities []string `json:"seed_entities,omitempty"`

	// RetightenRounds records how many times R2's retighten branch
	// fired before terminating (success, cap, or downstream branch).
	RetightenRounds int `json:"retighten_rounds,omitempty"`
}

DecompTrace records what decomposition the chain actually performed, for operator review of router quality. The shape is deliberately loose in Phase 1 — operator trajectories will dictate whether stricter typing is worth the churn (see plan doc PR 5 open-questions).

type DecomposeArgs

type DecomposeArgs struct {
	// Axes lists the dimensions of decomposition (e.g.
	// "entity_type", "time", "spatial", "relationship"). Free-form
	// strings; backend matches against its known decomposition
	// templates.
	Axes []string `json:"axes"`

	// Focus is the central concept to decompose around. Anchors the
	// backend's template selection — e.g. "sensor maintenance
	// events" or "drone telemetry anomalies".
	Focus string `json:"focus"`

	// Scope is the breadth of decomposition. One of the
	// DecomposeScope* constants. Empty resolves to "medium" at the
	// backend.
	Scope string `json:"scope,omitempty"`
}

DecomposeArgs is the typed view of RouteDecision.Args when Action == ActionDecompose. Intent-shaped per ADR-045 Amendment 2026-05-23: the model emits decomposition INTENT (what to decompose along, the central concept, the breadth); the backend (execute_subqueries) constructs the typed sub-queries from templates keyed on Axes + Focus. The model isn't asked to spell sub-query types it can't reliably produce.

type Evidence

type Evidence struct {
	// EntityID is the graph entity this evidence refers to. Required.
	EntityID string `json:"entity_id"`

	// Tier is the retrieval tier that surfaced this hit: "0" (rules /
	// predicate queries), "1" (BM25), or "2" (neural — deferred to
	// Phase 2). Operators may use it to filter evidence by retrieval
	// method.
	Tier string `json:"tier"`

	// Source is the retrieval source within the tier — e.g.
	// "classifier", "predicate_walk", "bm25_index". Required.
	Source string `json:"source"`

	// Score is the within-tier ranking score (higher = better).
	// Cross-tier comparison is not meaningful in Phase 1 (per-tier
	// ordering + recency tie-break); Phase 2 may add a learned ranker.
	Score float64 `json:"score,omitempty"`

	// SnippetText is a short inline preview (prompt-injection
	// friendly). Omit when the evidence has no readable preview.
	SnippetText string `json:"snippet_text,omitempty"`

	// ObjectStoreRef is the ObjectStore key for the full body when
	// the evidence has bulk content. Empty when the evidence is fully
	// expressed in the EntityID + triples on the graph.
	ObjectStoreRef string `json:"objectstore_ref,omitempty"`
}

Evidence is one item in SearchResult.Evidence — a single entity hit or evidence snippet the chain surfaced. Provenance is mandatory: every evidence item carries enough metadata (EntityID + Tier + Source) for the caller to verify it back against the graph and for synthesize_answer's quote-back validation to reject fabricated refs.

ObjectStoreRef is set when the body of the evidence (long text, document, etc.) lives in ObjectStore; consumers read it via that ref. SnippetText carries a short inline preview for prompt-injection readability without triggering ObjectStore round-trips on every hit.

func (*Evidence) Validate

func (e *Evidence) Validate() error

Validate checks the required fields and rejects unknown tier values.

type ExecutionOutput

type ExecutionOutput struct {
	// Topic echoes the originating Intent topic so downstream
	// consumers don't have to re-fetch the intent payload for
	// logging / display.
	Topic string `json:"topic"`

	// Action is the routing action that drove this execution. One
	// of the research.ActionXxx constants. Required.
	Action string `json:"action"`

	// Evidence is the dedup'd + ranked + budget-enforced evidence
	// set the fan-out produced. May be empty (the routing decision
	// may have produced no hits — assess_sufficiency surfaces this
	// to the LLM as "you have nothing to synthesize from"; the
	// chain doesn't error on empty).
	Evidence []Evidence `json:"evidence,omitempty"`

	// SubQueryCount is the number of sub-queries the materializer
	// produced for this execution. Operator observability: tracks
	// how aggressively decompose templates fanned out. Surfaces
	// drift in scope-cap behaviour (narrow shouldn't produce many).
	SubQueryCount int `json:"subquery_count,omitempty"`

	// Degraded flags incomplete fan-out — e.g. spatial axis in
	// decompose args (Phase 2 wire), a sub-query timeout, or a
	// retrieval-tier error. Sets DegradedReason to the per-cause
	// explanation. assess_sufficiency may downgrade confidence
	// when degraded.
	Degraded       bool   `json:"degraded,omitempty"`
	DegradedReason string `json:"degraded_reason,omitempty"`

	// BudgetTokensUsed reports the estimated token cost of the
	// emitted evidence array after budget enforcement. Operator-
	// facing observability for tuning Intent.BudgetTokens defaults.
	BudgetTokensUsed int `json:"budget_tokens_used,omitempty"`
}

ExecutionOutput is the execute_subqueries component's emit: evidence array + provenance metadata. R3 fires on the matching trigger key; assess_sufficiency (PR 5) consumes the payload to pick sufficient / refine.

Action carries the routing action that produced this execution (synthesize_directly / retighten / walk_seeds / decompose). The rule chain in PR 6 doesn't pattern-match on it — that's R2's job — but downstream operator observability (trajectory viewers, ops dashboards) wants to know which routing branch the evidence came from.

func (*ExecutionOutput) MarshalJSON

func (p *ExecutionOutput) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler with the alias-recursion guard.

func (*ExecutionOutput) Schema

func (p *ExecutionOutput) Schema() message.Type

Schema implements message.Payload.

func (*ExecutionOutput) UnmarshalJSON

func (p *ExecutionOutput) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler with the alias-recursion guard. Partial payloads (e.g. {}) round-trip clean; strict validation lives in Validate, not at decode time, so test + partial-update flows can decode incrementally without tripping shape checks the producer hasn't completed yet.

func (*ExecutionOutput) Validate

func (p *ExecutionOutput) Validate() error

Validate implements message.Payload. Enforces required fields (Topic always; Action when not Degraded) and per-Evidence shape via Evidence.Validate. Empty Evidence is valid (degraded path).

Action contract: required + must be a canonical RouteAction EXCEPT on the Degraded path where execute_subqueries didn't have a RouteDecision to consult (e.g. upstream classify.complete or route.complete never landed). Those envelopes ship with empty Action + Degraded=true + DegradedReason explaining the gap so assess_sufficiency (PR 5) can route to low-confidence handling without R3 stranding the loop on a hard chain-abort.

type Intent

type Intent struct {
	// Topic is the natural-language research question or subject.
	// Required.
	Topic string `json:"topic"`

	// Hints is an optional, free-form map of classifier hints. Phase 1
	// keeps the shape flexible; Phase 2 may add typed subtypes once
	// operator trajectories show what hints get passed.
	Hints map[string]string `json:"hints,omitempty"`

	// BudgetTokens caps total LLM token spend across the chain. Zero
	// falls back to DefaultBudgetTokens at consumption time so callers
	// can omit it.
	BudgetTokens int `json:"budget_tokens,omitempty"`

	// MaxIterations caps the refine loop in R4. Zero falls back to
	// DefaultMaxIterations.
	MaxIterations int `json:"max_iterations,omitempty"`
}

Intent is the caller's request: a free-form topic plus optional hints, plus per-call budgets. Per ADR-045's v2 amendment, the intent carries only what the caller actually knows — no entity IDs, no predicate names, no tier mix; those are outputs of the chain.

Hints is intentionally a map[string]string in Phase 1: trajectory data from operator use will inform whether typed hint subtypes are worth adding in Phase 2 (see plan doc Risks for PR 1). Keys are classifier-parseable strings ("entity_kind", "domain", "recency", etc.); unknown keys are passed through and ignored downstream rather than rejected, so operators can experiment without schema churn.

func (*Intent) MarshalJSON

func (p *Intent) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. Uses the alias pattern to avoid recursion through the Payload interface (the registry decodes via this method).

func (*Intent) ResolvedBudgetTokens

func (p *Intent) ResolvedBudgetTokens() int

ResolvedBudgetTokens returns BudgetTokens or DefaultBudgetTokens when zero. Use this at consumption time so callers can omit the field and the chain still has a budget cap.

func (*Intent) ResolvedMaxIterations

func (p *Intent) ResolvedMaxIterations() int

ResolvedMaxIterations returns MaxIterations or DefaultMaxIterations when zero.

func (*Intent) Schema

func (p *Intent) Schema() message.Type

Schema implements message.Payload.

func (*Intent) UnmarshalJSON

func (p *Intent) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. Mirrors MarshalJSON's alias pattern for the round-trip through the payload registry.

func (*Intent) Validate

func (p *Intent) Validate() error

Validate implements message.Payload. Required: Topic. Hints values are checked for non-empty strings when present (a hint key mapped to "" is a caller bug — empty signals "absent", which should be expressed by omitting the key).

type RetightenArgs

type RetightenArgs struct {
	// Topic is the refined topic string. Required.
	Topic string `json:"topic"`

	// Hints is the refined hint set. Optional; empty map and absent
	// field both treated as no hints.
	Hints map[string]string `json:"hints,omitempty"`
}

RetightenArgs is the typed view of RouteDecision.Args when Action == ActionRetighten. The refined topic + hints feed back into a second nl_classify run; R2's retighten branch caps the loop at MaxIterations=2.

type RouteDecision

type RouteDecision struct {
	// Action is the routing decision. Must be one of ActionXxx
	// constants. UnmarshalJSON and Validate both enforce the enum.
	Action string `json:"action"`

	// Args carries the action-specific argument map. Per-action shapes
	// (intent-not-structure per ADR-045 Amendment 2026-05-23 — model
	// emits intent, backend constructs typed structures):
	//   - synthesize_directly: empty (uses classifier output as-is)
	//   - retighten:           RetightenArgs   {topic, hints}
	//   - walk_seeds:          WalkSeedsArgs   {seeds: [{ref, ref_type}]}
	//                          ref_type ∈ {"name","partial_id","candidate_index"};
	//                          backend resolves to full 6-part entity IDs
	//   - decompose:           DecomposeArgs   {axes, focus, scope}
	//                          backend constructs typed sub-queries
	// The map is intentionally loose at the wire level so the payload
	// stays uniform; strict per-action shape checks live in the
	// Parse*Args helpers below, called by the consuming component
	// after a successful Validate() of the action enum.
	Args map[string]any `json:"args,omitempty"`

	// Rationale is the LLM's natural-language justification for the
	// chosen action. Captured for operator trajectory review;
	// rule-opaque per discipline memory.
	Rationale string `json:"rationale,omitempty"`
}

RouteDecision is route_search's structured-emit output: one of four routing actions, action-specific args, and a free-form rationale for trajectory review. Phase 1's PR 3 specializes the LLM prompt around these four actions; ADR-045 commits to the closed enum at the schema level (Phase 2 may add actions, but Phase 1 ships strict).

Per feedback_llm_authored_predicates_rule_opaque, when this payload is stamped onto loop entity triples, the Rationale field's triple-predicate is published with WithRuleOpaque(true). Rules should not pattern-match on rationale prose — they branch on the typed Action field only.

func (*RouteDecision) MarshalJSON

func (p *RouteDecision) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler with the alias-recursion guard.

func (*RouteDecision) ParseDecomposeArgs

func (p *RouteDecision) ParseDecomposeArgs() (DecomposeArgs, error)

ParseDecomposeArgs decodes p.Args into DecomposeArgs and validates shape. Returns an error if p.Action != ActionDecompose, if required fields are missing, or if Scope is non-empty and outside the closed enum.

func (*RouteDecision) ParseRetightenArgs

func (p *RouteDecision) ParseRetightenArgs() (RetightenArgs, error)

ParseRetightenArgs decodes p.Args into RetightenArgs and validates shape. Returns an error if p.Action != ActionRetighten, if Topic is empty, or if any Hints value is empty.

func (*RouteDecision) ParseWalkSeedsArgs

func (p *RouteDecision) ParseWalkSeedsArgs() (WalkSeedsArgs, error)

ParseWalkSeedsArgs decodes p.Args into WalkSeedsArgs and validates shape. Returns an error if p.Action != ActionWalkSeeds, if Seeds is empty, or if any seed has an empty Ref or an out-of-enum RefType.

func (*RouteDecision) Schema

func (p *RouteDecision) Schema() message.Type

Schema implements message.Payload.

func (*RouteDecision) UnmarshalJSON

func (p *RouteDecision) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler with strict action enum enforcement. Schema-violating values fail at decode rather than surfacing as nil-action downstream.

func (*RouteDecision) Validate

func (p *RouteDecision) Validate() error

Validate implements message.Payload. Enforces the closed action enum. Args content is not validated here — per-action arg shapes are checked by the consuming component (PR 3+).

type SearchResult

type SearchResult struct {
	// Evidence is the set of hits that informed Synthesis. Every
	// evidence item synthesize_answer references must appear here;
	// quote-back validation enforces this in PR 5.
	Evidence []Evidence `json:"evidence,omitempty"`

	// Synthesis is the natural-language answer produced by
	// synthesize_answer. Refs quoted in the prose must resolve to
	// Evidence items above.
	Synthesis string `json:"synthesis"`

	// DecompTrace is the per-call audit of the chain's routing
	// choices and decompositions. Optional — present when the chain
	// took a non-trivial path (anything beyond synthesize_directly).
	DecompTrace *DecompTrace `json:"decomp_trace,omitempty"`

	// TokensUsed is the total LLM-token spend across the chain's
	// LLM calls (route_search + assess_sufficiency + synthesize_answer
	// + any retighten/refine repetitions).
	TokensUsed int `json:"tokens_used,omitempty"`

	// Iterations is the total refine-loop iterations the chain
	// executed before terminating. Distinct from RetightenRounds in
	// DecompTrace — Iterations counts R4's refine loop (capped by
	// ResearchIntent.MaxIterations); RetightenRounds counts R2's
	// retighten branch (capped at 2).
	Iterations int `json:"iterations,omitempty"`
}

SearchResult is the chain's terminal output, returned to the parent loop via the continuation rule. Carries synthesis text plus the evidence refs synthesize_answer quoted back, plus the decomp trace for trajectory review.

func (*SearchResult) MarshalJSON

func (p *SearchResult) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler with the alias-recursion guard.

func (*SearchResult) Schema

func (p *SearchResult) Schema() message.Type

Schema implements message.Payload.

func (*SearchResult) UnmarshalJSON

func (p *SearchResult) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

func (*SearchResult) Validate

func (p *SearchResult) Validate() error

Validate implements message.Payload. Synthesis is required (the chain's terminal emit always carries at least the synthesized answer; failure paths emit a degraded result with an error string rather than an empty Synthesis). Evidence items are individually validated when present.

type SeedRef

type SeedRef struct {
	// Ref is the reference itself — a display name, a partial
	// federated ID, or a stringified index into the upstream
	// classifier candidate list. Interpretation depends on RefType.
	Ref string `json:"ref"`

	// RefType disambiguates how the backend resolves Ref. Closed
	// enum.
	RefType string `json:"ref_type"`
}

SeedRef carries one seed reference for WalkSeedsArgs. RefType is one of SeedRefType*.

type WalkSeedsArgs

type WalkSeedsArgs struct {
	Seeds []SeedRef `json:"seeds"`
}

WalkSeedsArgs is the typed view of RouteDecision.Args when Action == ActionWalkSeeds. Intent-shaped per ADR-045 Amendment 2026-05-23: the model emits SEED REFERENCES (names, partial IDs, or indices into the classifier candidate list); the backend resolves to full 6-part entity IDs via the entity index. The model isn't asked to spell full IDs it can't reliably produce.

Jump to

Keyboard shortcuts

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