reflex

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package reflex is the per-turn tier's client: the small structured calls a conversation makes AROUND an exchange rather than in it.

Three calls live here. Route runs before the turn — it reads the message just typed and the index of what is already remembered, and answers which remembered lines this turn actually needs. Extract runs after it — it reads the exchange and answers whether anything in it is worth keeping. Decide settles what to do with something worth keeping when the store already holds something near it.

They share one shape and one law:

  • THE MODEL IS A REFLEX, NOT A THINKER, AND THE PROMPT IS WHERE THAT IS SAID. Every call goes out with a small prompt on roles.RoleReflex — its own tier (roles.TierReflex) precisely because a call made twice per turn is a different economy from one made once a session. Nothing here asks the model to reason; it sorts, it names, and it answers in a few words of JSON. It used to say so on the WIRE as well, with a 200-token ceiling and a required reasoning disable; neither travels now, because how deeply somebody else's model thinks is not this package's to decide. An effort already carried by the caller or the configured model remains intact; this helper adds none.

  • REFLEX NEVER BREAKS A TURN. A model that answers with prose, with a code fence, or with an enum this package has never heard of costs ONE repair retry. A model that spends its ceiling and answers nothing gets ONE larger retry, then the session's low-tier fallback. After that every failure is a zero result and ErrReflexFailed. Every caller treats that as a no-op: the turn happens exactly as it would have if this package did not exist. That is why the failure is a typed error rather than a returned half-answer — there is no such thing as a partly-routed turn.

The package is the client only. It makes no decision about WHEN a call happens, holds no store, and is wired into no loop; the chat integration and the memory store are separate slices that build against the types below.

Index

Constants

This section is empty.

Variables

View Source
var ErrReflexFailed = errors.New("reflex: no usable answer")

ErrReflexFailed is every way a reflex call can fail to produce something usable: the provider refused, the model went quiet, or two attempts in a row were not a JSON object this package could read.

It is ONE error on purpose. A caller has exactly one thing to do with any of those — nothing — and a taxonomy would invite a turn loop to handle a provider timeout differently from a bad enum, which is the beginning of a reflex that can break a turn.

Functions

func AnsweredNothing

func AnsweredNothing(response *ai.Response) bool

AnsweredNothing is the one failure this package can do anything about: a call that came back with no visible text at all.

IT USED TO BE "EMPTY AT THE CEILING WE SENT", and that reading died with the ceiling. What is left is the honest half of it — there is nothing to decode, so there is nothing to use — and it is exported because the accounting wrapper that journals each paid reflex call must call a request empty on exactly the same terms this package does, or the bill and the recovery disagree about what happened (internal/session's memory.go).

func FallbackModel

func FallbackModel(src roles.Source, sessionDefault string) string

FallbackModel resolves the configured low tier without borrowing a role pin. A reflex model that cannot answer is a failure of that tier's model, so the fallback is the next economy itself rather than whichever unrelated low-tier role happens to have a private override.

func Model

func Model(src roles.Source, sessionDefault string) (string, error)

Model resolves which model the reflex calls run on: the roles ladder for roles.RoleReflex — its pin, then its tier, then the model the session is already talking to.

It is here rather than in the three call sites because the ladder is a property of the ROLE, and a caller that resolved it itself would be a second place that decides what "reflex" means.

Types

type Cmd

type Cmd struct{ Name, Arg string }

Cmd is an explicit instruction the person gave about memory itself. Name is one of "remember" or "forget".

type Completer

type Completer interface {
	CompleteWithMessages(ctx context.Context, messages []ai.Message, options ...ai.Option) (*ai.Response, error)
}

Completer is the one-method slice of the provider client this package needs. It is spelled the same way internal/plan ([plan.Completer]) and internal/session ([session.Completer]) spell it — one method, the same signature — so the client a session already holds satisfies it without an adapter, and a test satisfies it with a struct that returns a string.

It is declared HERE rather than imported from either of them because both of those packages would drag their whole world in: internal/reflex is called from a turn loop and must not be a reason internal/session cannot compile.

func Bind

func Bind(c Completer, model string) Completer

Bind returns a Completer that sends every request to one model.

The three calls take a plain Completer because that is the contract the chat and store slices build against, and a model has to reach the request somehow: a caller resolves once with Model, binds once with this, and hands the result to as many calls as it likes. A session's own client is left untouched, which matters — it is the same client the conversation runs on.

type DecideResult

type DecideResult struct {
	Op       string
	TargetID string
	Title    string
	Text     string
	Tags     []string
}

DecideResult is what to do with a candidate given what is already there.

func Decide

func Decide(ctx context.Context, c Completer, candidate ExtractResult, neighbors []Neighbor) (DecideResult, error)

Decide settles a candidate against what the store already holds near it: skip it, refine one of them, replace one of them, or add it.

A missing TargetID on an update or a supersede is NOT refused here — the enum is what this package validates, and the store is the only thing that knows whether an id is real. A caller that gets one treats it as a skip.

type ExtractResult

type ExtractResult struct {
	Mem   int
	Type  string
	Scope string
	Title string
	Text  string
	Tags  []string
	// State is present only when the exchange MOVED the work. A turn that
	// answered a question changed nothing about where the work stands.
	State *StateDelta
	// Used names the injected memory ids that actually BORE ON THE ANSWER, out
	// of the ones this turn was shown. It is asked of a model that is already
	// reading the exchange, so it costs no extra call and about ten output
	// tokens.
	//
	// It exists because counting a retrieval as a use credits a memory for
	// being INJECTED rather than for helping — RoMeRL (arXiv 2608.02508) names
	// that the "memory-reward trap", and fixing the credit assignment is what
	// shrank its memory pool 84.4% for +2.9pp accuracy. An id the model
	// invented is dropped, exactly as [RouteResult.Inject] drops one.
	//
	// It is empty on every turn nothing was injected, and empty is also the
	// honest answer when lines were shown and none of them mattered — the
	// caller reads that difference against what it injected.
	Used []string
}

ExtractResult is the post-turn answer.

Mem is the whole gate: 0 means the exchange held nothing worth carrying into another session, which is the answer for most exchanges, and every other field is then meaningless and unread.

func Extract

func Extract(ctx context.Context, c Completer, userMsg, assistantMsg string, injected []Stub) (ExtractResult, error)

Extract answers whether an exchange held anything worth remembering — and, when the turn was shown remembered lines, which of them actually bore on the answer.

injected is what the router put in front of the model for this exchange, and it is shown to the extractor for that second question alone. An empty list is the ordinary case and the question is then not asked at all: a heading with nothing under it reads to a small model like a list it failed to receive.

Mem 0 is returned as it arrived, with NO further validation of the memory fields: the model has said there is nothing here, they are whatever it left in them, and refusing that answer over a stray type would turn "nothing to remember" — the common case — into a retry on every turn. Used is read whatever mem says, because whether a memory helped and whether the exchange held something new are different questions about the same turn.

type Neighbor

type Neighbor struct{ ID, Title, Text string }

Neighbor is one thing the store already holds that sits near a candidate.

type RouteResult

type RouteResult struct {
	// Inject holds ids FROM THE INDEX IT WAS GIVEN. An id the model invented is
	// dropped rather than passed on — the store would not find it, and a
	// missing memory reported as an injected one is a lie the caller cannot
	// check.
	Inject []string
	// Cmd is nil on almost every turn.
	Cmd *Cmd
}

RouteResult is the pre-turn answer: which remembered ids belong in this turn, and the memory command the person typed, if they typed one.

func Route

func Route(ctx context.Context, c Completer, userMsg string, index []Stub) (RouteResult, error)

Route answers which remembered lines this turn needs, before the turn runs.

An empty list is the ordinary answer and is not a failure. A message with nothing in it is answered without a call at all: there is nothing to route against, and a reflex that bills for that would bill for every stray return.

type Session

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

Session holds the reflex failover facts that belong to one conversation. Provider quirks are process-wide because they describe a model; abandoning a model is session-local because another session may have changed its crew or may deliberately want to try it again.

func (*Session) Bind

func (s *Session) Bind(c Completer, model, fallback string, notice func(string)) Completer

Bind returns a completer that starts on model and, once that model has answered nothing at all, uses fallback for the rest of this session. notice is called only on the transition and never while a lock is held.

type StateDelta

type StateDelta struct {
	Goal     string
	Done     []string
	Inflight []string
	Next     []string
	Open     []string
	Refs     []string
}

StateDelta is what an exchange did to the shape of the work: where it is going, what has landed, what is in flight, what is next, what is still open, and what it points at.

type Stub

type Stub struct{ ID, Title, Type, Scope string }

Stub is one line of the memory index as the router sees it: enough to decide whether the line matters to this turn, and never the line's own text.

Jump to

Keyboard shortcuts

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