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
- func BuildAssessCompleteTriples(loopEntityID string, sufficient bool, ts time.Time) []message.Triple
- func BuildClassifyCompleteTriples(loopEntityID string, candidateCount int, degraded bool, ts time.Time) []message.Triple
- func BuildExecuteCompleteTriples(loopEntityID string, evidenceCount int, ts time.Time) []message.Triple
- func BuildKickoffTriples(loopEntityID, topic, loopID, parentLoopID, parentRole string, ...) []message.Triple
- func BuildRouteCompleteTriples(loopEntityID, action string, ts time.Time) []message.Triple
- func BuildSearchResultCompleteTriples(loopEntityID, resultRef string, ts time.Time) []message.Triple
- func IsValidDecomposeScope(s string) bool
- func IsValidRouteAction(s string) bool
- func IsValidSeedRefType(s string) bool
- func RegisterPayloads(reg *payloadregistry.Registry) error
- type AssessmentOutput
- type Candidate
- type ClassifierOutput
- type DecompTrace
- type DecomposeArgs
- type ExecutionOutput
- type Intent
- type RetightenArgs
- type RouteDecision
- func (p *RouteDecision) MarshalJSON() ([]byte, error)
- func (p *RouteDecision) ParseDecomposeArgs() (DecomposeArgs, error)
- func (p *RouteDecision) ParseRetightenArgs() (RetightenArgs, error)
- func (p *RouteDecision) ParseWalkSeedsArgs() (WalkSeedsArgs, error)
- func (p *RouteDecision) Schema() message.Type
- func (p *RouteDecision) UnmarshalJSON(data []byte) error
- func (p *RouteDecision) Validate() error
- type SearchResult
- type SeedRef
- type WalkSeedsArgs
Constants ¶
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.
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.
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.
const ( // SourceClassify is the Triple.Source for nl_classify orchestration stamps. SourceClassify = "research-graph-classify" // SourceRoute is the Triple.Source for route_search orchestration stamps. SourceRoute = "research-graph-route" // SourceExecute is the Triple.Source for execute_subqueries orchestration stamps. SourceExecute = "research-graph-execute" // SourceAssess is the Triple.Source for assess_sufficiency orchestration stamps. SourceAssess = "research-graph-assess" // SourceSynthesize is the Triple.Source for synthesize_answer orchestration stamps. SourceSynthesize = "research-graph-synthesize" )
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.request.received" // 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.request.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.request.hint" // PredicateResearchBudgetTokens carries the resolved per-call token // budget (after defaulting). Stored as string per Triple.Object // shape; consumers parse to int. PredicateResearchBudgetTokens = "research.request.budget-tokens" // PredicateResearchMaxIterations carries the resolved refine-loop // cap (after defaulting). PredicateResearchMaxIterations = "research.request.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 = "agent.loop.role" // PredicateResearchLoopID stamps the raw research-pipeline loop ID // (e.g. "rg_abc12345") on the loop entity as a dedicated triple // so R0-R6 rules can substitute it into NATS publish subjects via // `$entity.triple.research.loop.id`. The 6-part loop-execution // entity ID has the loop ID as its last segment, but the // substitution layer doesn't expose a "last-segment" accessor and // the rules need the raw form for routing. Cheap one-extra-triple // per chain instead of an engine feature. PredicateResearchLoopID = "research.loop.id" // PredicateResearchParentRole stamps the parent loop's role so the // continuation rule (R6) can route the SearchResult back to the // correct caller without re-fetching the parent LoopEntity from // AGENT_LOOPS. Empty when the research_graph tool was invoked from // outside an agent loop (rare). PredicateResearchParentRole = "research.parent.role" // PredicateResearchClassifyComplete marks nl_classify completion. // Stamped together with PredicateResearchClassifyCandidateCount // and PredicateResearchClassifyDegraded. PredicateResearchClassifyComplete = "research.classify.complete" // PredicateResearchClassifyCandidateCount is the number of candidate // entities the classifier surfaced. Stored as a string per Triple.Object // shape; consumers parse to int. PredicateResearchClassifyCandidateCount = "research.classify.candidate-count" // PredicateResearchClassifyDegraded marks "true" when the classifier // fell back to a degraded path (e.g., semantic-fallback fired). // "false" on the happy path. PredicateResearchClassifyDegraded = "research.classify.degraded" // PredicateResearchRouteComplete marks route_search completion. // Stamped together with PredicateResearchRouteAction. PredicateResearchRouteComplete = "research.route.complete" // PredicateResearchRouteAction is one of ActionSynthesizeDirectly / // ActionRetighten / ActionWalkSeeds / ActionDecompose. R2 branches // on this value via action-level when clauses (ADR-041). PredicateResearchRouteAction = "research.route.action" // PredicateResearchExecuteComplete marks execute_subqueries completion. // Stamped together with PredicateResearchExecuteEvidenceCount. PredicateResearchExecuteComplete = "research.execute.complete" // PredicateResearchExecuteEvidenceCount is the number of evidence // items the execute stage assembled (post-dedup, post-budget). Stored // as a string; consumers parse to int. PredicateResearchExecuteEvidenceCount = "research.execute.evidence-count" // PredicateResearchAssessComplete marks assess_sufficiency completion. // Stamped together with PredicateResearchAssessSufficient. PredicateResearchAssessComplete = "research.assess.complete" // PredicateResearchAssessSufficient is "true" or "false". R4 branches // on this value via action-level when clauses (ADR-041): sufficient // → synthesize_answer; insufficient → execute_subqueries (refine // loop, bounded by per-action MaxIterations=5). PredicateResearchAssessSufficient = "research.assess.sufficient" // PredicateResearchSearchResultComplete marks synthesize_answer // completion — the chain's terminal stage. R6 (continuation) // triggers on this predicate's appearance and routes the // SearchResult back to the parent loop via publish_agent. PredicateResearchSearchResultComplete = "research.search-result.complete" // PredicateResearchSearchResultRef is the KV key where the // SearchResult envelope lives (search_result.complete.<loopID> in // AGENT_LOOPS). R6's continuation publishes this ref to the parent // so the parent's next iteration can read_loop_result on the // SearchResult without it riding in the rule payload (per ADR-028: // rules carry references, not content). PredicateResearchSearchResultRef = "research.search-result.ref" // PredicateResearchEvidencePresent is the rule-authored evidence marker. PredicateResearchEvidencePresent = "research.evidence.present" // PredicateResearchStateStatus is the rule-authored terminal/degraded status. PredicateResearchStateStatus = "research.state.status" )
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.
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.
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.
const DefaultBudgetTokens = 4000
DefaultBudgetTokens is ResearchIntent.BudgetTokens default per docs/operations/22-adr045-phase1-plan.md PR 1 spec.
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.
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.
const SourceResearchGraphTool = "agent-research-graph"
SourceResearchGraphTool is the Triple.Source for the kickoff triples stamped by the research_graph agent tool when a chain spawns. Distinct from the per-component Source values so operator dashboards can attribute the chain entry point separately.
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 BuildAssessCompleteTriples ¶
func BuildAssessCompleteTriples(loopEntityID string, sufficient bool, ts time.Time) []message.Triple
BuildAssessCompleteTriples returns the orchestration triples assess_sufficiency stamps after its AssessmentOutput envelope is durably written to AGENT_LOOPS. R4's action-level when clauses match on PredicateResearchAssessSufficient.
func BuildClassifyCompleteTriples ¶
func BuildClassifyCompleteTriples(loopEntityID string, candidateCount int, degraded bool, ts time.Time) []message.Triple
BuildClassifyCompleteTriples returns the orchestration triples nl_classify stamps after its candidate set is durably written to AGENT_LOOPS. Three triples share the loop entity Subject and land atomically. R1 fires on PredicateResearchClassifyComplete's false→true transition.
func BuildExecuteCompleteTriples ¶
func BuildExecuteCompleteTriples(loopEntityID string, evidenceCount int, ts time.Time) []message.Triple
BuildExecuteCompleteTriples returns the orchestration triples execute_subqueries stamps after its evidence array is durably written to AGENT_LOOPS. R3 fires on PredicateResearchExecuteComplete's false→true transition. evidenceCount lets ops dashboards spot zero-evidence executes (likely router misclassification) without re-fetching the envelope.
func BuildKickoffTriples ¶
func BuildKickoffTriples(loopEntityID, topic, loopID, parentLoopID, parentRole string, budgetTokens, maxIterations int, ts time.Time) []message.Triple
BuildKickoffTriples returns the triple batch the research_graph tool stamps on the research-pipeline loop entity at chain spawn time. R0 fires on PredicateResearchRequested's false→true transition (and loop.role gates which loops the rule scopes to). Triples are:
- loop.role = "research_pipeline" (gates R0 scope to this chain)
- research.request.received = "true" (the trigger marker)
- research.request.topic = <topic>
- research.loop.id = <loopID> (rule substitution into NATS subjects)
- research.parent.loop = <parentLoopID> (continuation lineage)
- research.parent.role = <parentRole> (R6 publish_agent role)
- research.request.budget_tokens = <int>
- research.request.max_iterations = <int>
All triples share the loop entity Subject so the batch lands atomically — R0 must not see a half-populated chain identity.
parentRole may be empty when the research_graph tool is invoked outside an agent loop; R6 then falls back to a configured default.
func BuildRouteCompleteTriples ¶
BuildRouteCompleteTriples returns the orchestration triples route_search stamps after its RouteDecision envelope is durably written to AGENT_LOOPS. Two triples share the loop entity Subject and land atomically — R2's action-level when clauses match on PredicateResearchRouteAction so a partial write (action without complete, or complete without action) would corrupt the dispatch. Action must be one of the four research.Action* constants; callers pass the validated RouteDecision.Action verbatim.
func BuildSearchResultCompleteTriples ¶
func BuildSearchResultCompleteTriples(loopEntityID, resultRef string, ts time.Time) []message.Triple
BuildSearchResultCompleteTriples returns the orchestration triples synthesize_answer stamps after its SearchResult envelope is durably written to AGENT_LOOPS at the search_result.complete.<loopID> key. R6 (continuation) fires on PredicateResearchSearchResultComplete and publishes back to the parent loop with resultRef as the payload_ref (per ADR-028: rules carry references, not content). resultRef is the AGENT_LOOPS key (e.g., "search_result.complete.rg_abc12345").
func IsValidDecomposeScope ¶
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 ¶
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 ¶
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 complete research payload family with the supplied registry. Production composition roots call it through graphresearch.RegisterPayloads only when graphresearch.Selected reports that the deployment selected the graph-research capability; it is intentionally absent from the unconditional payloadbuiltins registry.
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 graph.query.searchGraph entity-digest shape (entity_id + label + type + relevance score + optional snippet) so PR 4 (execute_subqueries) can consume nl_classify's output and the graph-query operation'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.
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 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 []fusion.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 ¶
MarshalJSON implements json.Marshaler. Uses the alias pattern to avoid recursion through the Payload interface (the registry decodes via this method).
func (*Intent) ResolvedBudgetTokens ¶
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 ¶
ResolvedMaxIterations returns MaxIterations or DefaultMaxIterations when zero.
func (*Intent) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler. Mirrors MarshalJSON's alias pattern for the round-trip through the payload registry.
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 []fusion.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.