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 ¶
- Variables
- func AnsweredNothing(response *ai.Response) bool
- func FallbackModel(src roles.Source, sessionDefault string) string
- func Model(src roles.Source, sessionDefault string) (string, error)
- type Cmd
- type Completer
- type DecideResult
- type ExtractResult
- type Neighbor
- type RouteResult
- type Session
- type StateDelta
- type Stub
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.