remember (remember.md) and query (query.md) are two different operations, but they are built the same way: each is a pipeline, a fixed list of named stages that run one after the other on the state of one call, and each call leaves a run log, a row that says how every stage went. This page describes that common pattern. What each stage of each operation does is in its own page; the code of the pattern is the package internal/memory/memory_pipeline.
The image pipeline (images.md) does not use it, and has no run log.
The pattern in one picture
- Setup (
Remember,Query): validates the request, resolves the dataset, the session, the models and reasoning of each stage (memory_core.CallOptions), starts the trace of the call and creates the run log row. A request rejected here (a400, an unknown dataset) has no run; one that was not rejected has. - The pipeline: a value, a list of steps, run by
memory_pipeline.Run. It is where the work is. - Finish: the deferred
run.Finish(err)closes the run, whatever happened:succeededif the call returned a result,failed(with the error) if it returned an error, and no stage is left open.
Declaring a pipeline
A pipeline is a[]memory_pipeline.Step[S], where S is the job: the struct that holds the state of one call. The list reads top to bottom as the operation does; its comments say what each stage is for. Query’s, in short:
memory_pipeline.Stage) has:
Two more things shape the flow:
memory_pipeline.Stop: a stage returns it to end the pipeline early without an error. The stage ends assucceeded, the stages after it areskipped, and the call returns normally.queryuses it when nothing was retrieved: there is nothing to answer from, so the fixed “no information” answer is the result and the LLM is not called.memory_pipeline.Each: runs a group of stages once for every item of the call (every chunk of a document), each item to the end before the next. AnEachis not a stage of the run log; the stages inside it are, and they start again for each item (a stage keeps its first start time, so it spans all the items).rememberuses it to extract and summarize every chunk, and then to reconcile and store them one after the other, so that a chunk is compared with the facts the earlier chunks stored.
reconcileSteps (load_candidates, resolve_edges, store) is run by the Each of the pipeline for the text or each chunk, and, with memory_pipeline.Run, by the enrichment stages (web_search, observe_series) for the texts they make.
What the runner does for every stage
memory_pipeline.Run(ctx, steps, job, observer) does, for each stage in order:
- If an earlier stage returned
Stop, or the stage is notEnabled:observer.Skip(stage)and go on. observer.Start(stage)(and of itsAlsostages), and give the stage a context with an inference sink.- Call
Run. observer.Done(err, stage). Then, depending on the error and the policy: end the pipeline with the error (Fatal), or go on (Advisory, or no error).
Run returns the error of the first Fatal stage that failed, and nil otherwise.
This is the point of the pattern: the stages do not know the run log. A stage cannot forget to open or close itself in the log, cannot leave itself open on an early return, and cannot be named wrong, because the runner does all of it, around the stage, from the list. Adding a stage is adding one line to the list (and its column group to the run table); the run log, the admin API and the admin UI follow.
Inferences
A stage that calls a model (an LLM, an embedding request, a web search) makes inferences (gb_inferences, see inferences.md). The context the runner hands to a stage carries a sink (memory_core.WithInferenceSink), and memory_core.RecordRun (which every inference is recorded through) passes it the ID of each one. The sink is observer.AddInference(stage, id), so every inference ends up in the <stage>_inference_ids of the stage that made it, in any goroutine of the stage, with no call to the log from the stage. A stage must therefore use the context it is given for its model calls (and pass it on); a call made with another context is not filed.
The run log
Both run logs implement thememory_pipeline.Observer interface (Start, Done, Skip, AddInference), and have the same shape:
- one row per execution of the pipeline in a table of its own (
gb_remember_runs,gb_query_runs): the call (user, dataset,params= how it was made), the status of the call as a whole (started, thensucceededorfailedwith anerror) and, for every stage of the pipeline, the columns<stage>_status,<stage>_error,<stage>_started_at,<stage>_finished_atand, for stages that run a model,<stage>_inference_ids; - the row is created when the call starts and updated at every step, so a call in progress can be watched, and a row that stays
startedbelongs to a process that died; - the status of a stage is one of
not_started(the pipeline never reached it: an earlier stage failed, or the call is still on its way),started,succeeded,failedorskipped(the call does not use it, or the pipeline was ended withStop). A run that succeeded has no stage leftnot_started; - a stage that is
Advisorycan befailedin a run that issucceeded: to know whether such a stage worked, look at the stage, not at the status of the call; - a
nillog does nothing, and a row that cannot be written is logged: the log never fails the call; - the stages come from a table of definitions (
relational.RememberStages,relational.QueryStages: the name, and whether the stage runs models). Tests check that the stages of each pipeline are exactly those of its table (TestPipelineStagesAreTheRunLogs), so the list and the schema cannot drift apart.
GET /v1/gb/admin/api/remember-runs and GET /v1/gb/admin/api/query-runs (counts by status, and the list, filtered and paged), and the admin UI draws both with the same code.
Existing database files are not migrated when the stages of a log change: delete the file and the server recreates it.
In the admin UI
The remember and query views (admin_ui/src/runs.ts) are the same view over the two logs: per database, how many runs are in each status (click a tile to filter by it), and the list of the runs, newest first. A run opens to its parameters (and, for a query, the question and the session) and a table with every stage: status, start, duration, inference IDs, error, and a ? button that says what the stage does. For remember there is also an Outputs button per stage, which loads the stage outputs; query keeps none. A run that succeeded but has a failed stage is flagged.
Adding an operation or a stage
To add a stage to an existing pipeline:- Add its name to the stage table (
relational.RememberStagesorQueryStages, withModeltrue if it runs models) and the columns to the schema of both backends (SQLite and Postgres). - Write its work as a method of the job, using the context it gets for model calls; return
memory_pipeline.Stoponly to end the call early on purpose. - Add a
memory_pipeline.Stageto the list, at its place, withPolicy: Advisoryif the call can succeed without it, andEnabledif only some calls use it. - Update the
?text of the stage in the admin UI (runs.ts), the docs of the operation, and the changelog.
job, its list of stages and its runLog (the Observer, around a relational run type with its table), Run the list from the operation’s entry point after the setup, and Finish the log with the error of the call in a defer.