Skip to content

xmemory: configurable recall read mode + per-sub-query answers from reader_results#177

Open
familom wants to merge 2 commits into
ClickHouse:mainfrom
familom:familom/xmemory-add-read-mode-and-reader-results
Open

xmemory: configurable recall read mode + per-sub-query answers from reader_results#177
familom wants to merge 2 commits into
ClickHouse:mainfrom
familom:familom/xmemory-add-read-mode-and-reader-results

Conversation

@familom

@familom familom commented Jul 13, 2026

Copy link
Copy Markdown

What

Adds an xmemory.read_mode setting to the optional xmemory memory layer, and teaches the bridge to consume the per-sub-query answers that xmemory-ai 0.10.0 returns.

Two commits, split by what each actually depends on:

Commit Needs
xmemory: make the recall read mode configurable nothing new — all three read modes have existed in the SDK since 0.4.0, so this passes on the existing xmemory-ai>=0.5.3 pin
xmemory: surface per-sub-query answers from reader_results xmemory-ai>=0.10.0 (bumped here)

The version bump lives in the commit that forces it. If the 0.10.0 upgrade ever has to be reverted, the read_mode work survives independently — I verified the first commit is green on the old pin.

Why

memory_recall hardcoded xmemory's SINGLE_ANSWER read. An instance whose value is in its rows (rather than in a synthesized sentence over them) had no way to say so. read_mode exposes the SDK's existing single-answer / raw-tables / xresponse choice.

Separately, xmemory-ai 0.10.0 decomposes a composite query ("who leads sales, and where is HQ?") into sub-queries and answers each, returning them as reader_results alongside the combined reader_result. Recall now renders each sub-answer under the sub-question it answers, so the agent gets an unambiguous mapping rather than a flat run of sentences.

Design notes

Config values mirror the SDK's wire values (single-answer, not single_answer), so ReadMode(mode) resolves them directly instead of via a mapping table — a mode added to the SDK later needs no change here. Underscores are accepted as an alias so an existing read_mode: raw_tables keeps working rather than silently degrading to the default, and an unknown value falls back to the default with a warning.

Rendering is keyed off the mode, not sniffed from the payload shape. Only single-answer carries an answer envelope to unwrap; the other modes return tables, rendered as JSON. This matters: shape-sniffing would descend into raw-tables rows and mistake a data row's answer column for the synthesized answer, silently dropping the rest of the row. A memory store with Q&A-shaped rows would return "30 days\n99.9%" and throw the questions away.

A failed sub-query is reported, not dropped. When the server answers some sub-queries and sets error on another, recall emits (unavailable: no data) for it. An explicit "not known" is worth more to the agent than a silently missing sub-query, and it stops a partial failure from reading as a complete answer.

Falls back to the combined reader_result when the query wasn't decomposed, when the server predates decomposition (empty list), or when no sub-query yielded anything usable — otherwise a read where every sub-query failed would discard an answer the server did produce.

Behavior change

None by default. read_mode defaults to single-answer, so anyone with xmemory configured and no explicit read_mode sees byte-identical recall output. The whole feature is opt-in.

The one visible change: the recall section label drops synthesized answer for a plain [xmemory], since under the structured modes the payload is rows, not an answer.

Known sharp edge (not addressed here)

memory_recall clips the xmemory section to 4 KB via _clip_to_budget, which cuts at a raw byte offset. That's fine for prose, but if you opt into raw-tables the JSON can be cut mid-token and reach the model malformed:

[{"city": "Berlin", "head": "Ann"}, {"city": "Züri
… (truncated to fit recall size budget)

Left alone because it's off the default path. The fix would be to truncate at row granularity (drop whole rows, note showing 12 of 47 rows) so the JSON stays parseable. Happy to do it here if reviewers prefer.

Testing

Full suite green: 1548 passed, 2 skipped. Each commit passes independently (1545 at the first, 1548 at the tip).

The bridge tests go from 21 to 31. The 10 new ones cover the cases the original tests missed — none of them exercised a decomposed read, an errored sub-query, or a non-single-answer payload:

  • sub-query answers keep their labels
  • an errored sub-query surfaces rather than vanishing
  • fallback to the combined answer when no sub-query yields anything
  • raw-tables rows are not mined for answer keys
  • non-ASCII survives JSON rendering (ensure_ascii=False)
  • read-mode resolution: hyphens, underscore alias, case, whitespace, unknown/empty → default
  • the default is pinned end-to-end through recall_answer

Note: the repo's documented pytest tests/ -v does not currently run — uv run pytest fails to resolve because pyproject.toml declares requires-python = ">=3.12" while memu-py==1.4.0 requires >=3.13. That reproduces on clean main and is unrelated to this PR, so I left it alone; requires-python = ">=3.13" fixes it if someone wants to pick it up.

familom added 2 commits July 13, 2026 18:07
`memory_recall` hardcoded xmemory's SINGLE_ANSWER read. Expose the mode as
`xmemory.read_mode` so an instance can instead return its structured rows
(`raw-tables`) or `xresponse`. Default stays `single-answer`, so recall output
is unchanged for anyone who does not set it.

Config values mirror the SDK's own wire values, so ReadMode resolves them
directly rather than through a mapping table — a mode added to the SDK later
needs no change here. Underscores are accepted as an alias, and an unknown
value falls back to the default with a warning.

Rendering is keyed off the mode rather than sniffed from the payload shape:
only `single-answer` carries an `answer` envelope to unwrap, while the other
modes return tables, rendered as JSON. Sniffing would misread a data row with
an `answer` column as the synthesized answer and drop the rest of the row.

The recall section label drops "synthesized answer" for a plain `[xmemory]`,
since under the structured modes the payload is rows, not an answer.
xmemory-ai 0.10.0 decomposes a composite query ("who leads sales, and where is
HQ?") into sub-queries and answers each, returning them as `reader_results`
alongside the combined `reader_result`. Render each sub-answer under the
sub-question it answers, so recall hands the agent an unambiguous mapping
rather than a flat run of sentences.

Requires the bump to xmemory-ai >=0.10.0; `reader_results` does not exist
before it. Everything else here works on the old pin, hence the separate commit.

A sub-query the server could not answer carries an `error` while its siblings
still succeed. Report it as "(unavailable: ...)": an explicit "not known" is
worth more to the agent than a silently missing sub-query, and it stops a
partial failure from looking like a complete answer.

Fall back to the combined `reader_result` when the query was not decomposed,
when the server predates decomposition (the list is empty), or when no
sub-query yielded anything usable — otherwise a read where every sub-query
failed would discard an answer the server did produce.
@CLAassistant

CLAassistant commented Jul 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants