Status: v1, v2 and theThe goal is to letseries_observationpart of v3 are implemented; embedding series andqueryintegration (the rest of v3) are not. v2 (providers and the fetch stage) differs from the sketch below: see Changes made while building v2; the observations differ too: see Changes made while building v3. v1: What exists is described in Timeseries: supplied series, anchors (artifact, entity, edge) resolved afterStore, stats, thePOST /v1/gb/timeseriesendpoint and two read endpoints. This file keeps the reasoning behind it and the plan for what is left: the provider fetch stage (v2), and derivedseries_observationartifacts (v3, done), then embedding andquery(v3, still to do). Where the text below differs from Timeseries, that page is right (see Changes made while building v1).
POST /v1/gb/remember associate timeseries data (not just text or images) with the events, entities and facts it remembers, so a memory such as “Apple fell sharply after the earnings miss” can be tied to Apple’s price series around that date.
Related: Remember, Artifacts, Images (a separate pipeline with its own tables, the model for this), Web search (a post-store enrichment stage, the model for fetching), Temporal facts.
Summary
Support both ways of getting a series, as different mechanisms:- Supplied series. The caller sends points in the request. Anchoring is resolved after extraction, so it works although the entities do not exist yet when the request is made.
- Fetched series. An opt-in enrichment stage after
Storeresolves entities to series through a provider (for example entity to ticker to price series). It follows theweb_searchpattern. - A standalone attach endpoint. It covers “I only know the entities after
rememberreturns”, refreshes live series, and takes large uploads.rememberuses the same code path internally.
The timing problem
The entities of a request do not exist until extraction andStore have run. A series can still be anchored at three levels:
If a selector matches nothing after extraction, the series field
on_unmatched decides: create (default), ignore or error. create is the default because “this is AAPL’s price series” implies the entity Apple even if the text never named it.
Data model
Points are not stored in the graph. Following the image pipeline, series have their own tables and are linked to the graph; they are not rows ofgb_artifacts.
- Many-to-many anchors, like
gb_entity_artifacts. One price series can anchor to an entity, to several news artifacts, and to the edges that describe events. - The window lives on the anchor, not on the series. What matters for memory is “the price in the 30 days around this news item”. The series is shared; each anchor says which slice matters.
- Series identity is (
dataset,provider,key,field,frequency). Re-sending or re-fetching upserts points (as the image SHA-256 deduplicates images). fieldsupports OHLCV and multi-metric series without a table per shape.- Bi-temporal points.
tsis event time. An optionalrecorded_atcan be added later for restated data (macro revisions). It is not in v1, but nothing should be designed to exclude it. gb_series_statsholds deterministic summaries computed in Go: min, max, last, percent change, trend, change points. These are what extraction, embedding andquerysee. The LLM never sees raw points.- Embed the description plus the stats, never the points, for example “AAPL daily close, Jan 2025, -6.2%, largest drop on 2025-01-31”.
Request
Additions to theremember request:
timeseries.series[](supplied): points are[ts, value]pairs to keep the JSON small. A series withoutanchorsis anchored to the artifact. The 1 MiB body limit applies, so there is a cap on points per series; large series go through the attach endpoint.timeseries.fetch(fetched): opt-in, likeweb_search. It costs money and time and can fail, so nothing is fetched by default.models.resolve_series(optional): the model for symbol resolution, if an LLM is used for it.- Errors:
400for malformed points, timestamps or anchors;503iffetchis asked for and no provider is configured.
Pipeline placement
- Supplied series are stored inside step 3’s transaction. Entity and edge selectors can only be resolved once the rows exist, and a bad series should fail the request before commit. Nothing is fetched, because the points are in the request.
- Fetched series run after the commit, like
web_search. A provider failure never failsremember; it is reported in the response and the eval trajectory.
The fetch stage
Modelled oninternal/memory/memory_remember/remember_websearch.go:
- Pick key entities, reusing the web-search heuristic: skip
concept, rank by number of edges of the call, cap at about 3. - Resolve: ask each requested provider whether it supports the entity and which series it maps to.
- Fetch the window around
reference_time(an event-study shape: before, at and after the event), and optionally the latest window. - Store: upsert the series and points, add anchors, compute stats.
- Embed the series description and stats.
Providers
Resolve maps an organization to a ticker and Fetch calls a price API (Polygon, Alpha Vantage, Yahoo) or FRED for macro series.
- Entity to ticker is where this goes wrong. A guessed ticker for “Apple” can be the wrong company. Prefer precision over recall: require an unambiguous match and skip otherwise.
- Cache the resolved identifier on the entity (an identifier attribute, or a
has_tickeredge), so it is resolved once. - Providers by entity type. Once ontologies are used by
remember, a class (Company) can imply a provider.
The attach endpoint
- Anchors by ID or selector to things that already exist, such as the entity names returned by an earlier
remember. - Has a fetch mode:
{"entity_id": 12, "provider": "prices"}. - Refreshes live series.
remembershould not own ongoing updates. - Accepts bigger bodies (or a file upload) than
remember. rememberis defined as “extract, then attach”, so there is one code path.
Making series part of the graph
v1 writes no edges from raw points. A later step (built in v3, see Changes made while building v3) adds a derived artifact, in the style ofweb_search:
- A new artifact kind,
series_observation, withparent_idpointing at the anchoring artifact and a definition or query field. - Its text is generated deterministically from the stats (“AAPL closed down 6.2% on 2025-01-31, its largest one-day drop in 90 days”).
- That text goes through the normal extract, reconcile and store path, so it yields bi-temporal edges, deduplication and embeddings with no new logic.
query would return the series anchored to the entities of the queried subgraph: summary and stats by default, downsampled points on request, through GET /v1/gb/timeseries/{id}?from&to&resolution.
Changes made while building v1
The design above was simplified in these ways:- No
fieldcolumn on points.fieldis a column ofgb_seriesand part of the series identity (provider,key,field); an OHLCV series is several series with the same key. Points are(series_id, ts, value).frequencyis only a label and is not part of the identity. - Stats are a JSON column of
gb_series(stats), over all the points, instead of agb_series_statstable with a window. A window’s stats are computed on read (GET /v1/gb/timeseries/{id}?from=&to=returnswindow_stats). Change points are replaced by the singlelargest_move. - Anchors have one real foreign-key column per target type (
artifact_id,entity_id,edge_id, exactly one set) instead ofanchor_typeandanchor_id, so deleting a target cascades to its anchors. on_unmatcheddefaults depend on the anchor:createfor an entity selector,ignorefor an edge selector,errorfor an ID.createis not allowed for an edge.- The attach request takes
seriesas a list, each with its own anchors, likeremember, rather than one series with top-level anchors. Its dataset must exist (404); a series without anchors is stored unanchored. - Not done in v1: embedding the series description and stats (the entity that an anchor creates is not embedded either), and the
models.resolve_seriesstage (there is no LLM step).timeseries.fetchis a400. - Chunked text: the series are stored after the chunks, anchored to the parent artifact, in a transaction of their own, so they are not atomic with the chunks.
- Lookahead leakage (risk 1): series are stored as supplied and never clipped at
reference_time; clipping is left to the caller until a fetch stage exists.
Changes made while building v2
- Providers are registered by the embedding program, with
ServeOptions.TimeseriesProviders, not configured: a provider is{Name, Description, QueryFormat, Fetch}. The sketchedResolvemethod is gone: an LLM decides which provider is used for which entity (theresolve_seriesstage), from the natural-languageDescriptionof each provider, and writes the provider-specific query (a ticker) from itsQueryFormat.Fetchreceives the entity, that query and the period. - No entity-to-identifier cache yet: the decision is repeated for every request.
- Entities offered: up to 20, by number of facts, with no type excluded (a provider may be for concepts). At most 10 fetches per request.
- Lookahead (risk 1) is decided: a fetch reaches
before(default 30 days) before the reference time andafter(default none) after it, and what a provider returns is clipped to that period. - Anchors: the fetched series is anchored to the entity and to the artifact the entity came from, both with the period as the window.
- The standalone endpoint has no fetch mode yet.
Changes made while building v3
Only the derivedseries_observation artifacts are built; embedding the series and query are on hold (see the rollout table). What was built is described in Timeseries; where it differs from the sketch in Making series part of the graph:
- Opt-in with
timeseries.observations, at most 10 per request. The sketch did not say; each observation is an extraction call, as a web search is a search, so nothing is derived by default. - It describes what the request wrote (the period of the supplied points, or the fetched period), not the whole stored series, and uses the stats of those points computed on read in Go (
Summarize), not the storedgb_series.stats. “Its largest one-day drop in 90 days” is therefore not said: the text only knows the window it was given (largest move between consecutive points, high, low, average, change, trend). - The text names a subject: the entity of the series’ first entity selector (supplied), or the entity it was fetched for. Without it the extraction has nothing to attach the facts to.
- A column for the series:
gb_artifacts.series_id(→gb_series,ON DELETE SET NULL), so the chain fact → observation → series is a real link and not a convention;querysays which series and window. The sketch’s “definition or query field” isquery. The observation is a child of the artifact that anchors the series: the call’s artifact (the parent, for a chunked text) for a supplied series, the artifact the entity came from for a fetched one.valid_atis the last point of the window. - Last stage, after the web search and the fetch (it needs the fetched series), and like them it never fails
remember. - Not done: a kind-specific treatment in
query(it sees an observation’s facts like any other fact), an observation by the standalone endpoint, deduplication of observations.
Rollout
Files to add, following the existing layout (v3 added
memory_remember/remember_observations.go and memory_timeseries/observation.go):
internal/memory/memory_timeseries/(parse, anchor, stats, fetch)internal/db/graph/{sqlite,postgres}/db_timeseries.gointernal/api/timeseries/docs/timeseries.md(now Timeseries)
Risks and open questions
- Lookahead leakage. If a series extends past
reference_time, a memory of an event contains the future. Either clip fetched windows atreference_time, or label the post-event part explicitly. Decision needed. - Scale. Points in SQL tables are fine to millions of rows. A dedicated store (Timescale, Parquet files like the image directory) is an alternative. Proposed: start with SQL.
- Licensing and rate limits. Market data providers restrict storing and redistributing data.
- Scope of v1. Supplied-only first, or fetch immediately? Is there a specific finance provider or key? Decision needed.
- Deletion. Deleting an anchor artifact or entity should remove its anchor rows. A series that other anchors still use should stay; one with no anchors can be removed.
- Time handling. Timezones, irregular sampling and units need explicit rules (UTC RFC 3339 only, as everywhere else;
frequencyis a label, not a constraint).