research

package
v1.0.0-beta.162 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 10 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 (
	// 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"
)
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.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.
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 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.

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 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

func BuildRouteCompleteTriples(loopEntityID, action string, ts time.Time) []message.Triple

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

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 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.

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 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

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 []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.

Jump to

Keyboard shortcuts

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