xmemory: configurable recall read mode + per-sub-query answers from reader_results#177
Open
familom wants to merge 2 commits into
Open
Conversation
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an
xmemory.read_modesetting to the optional xmemory memory layer, and teaches the bridge to consume the per-sub-query answers thatxmemory-ai0.10.0 returns.Two commits, split by what each actually depends on:
xmemory: make the recall read mode configurablexmemory-ai>=0.5.3pinxmemory: surface per-sub-query answers from reader_resultsxmemory-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_modework survives independently — I verified the first commit is green on the old pin.Why
memory_recallhardcoded xmemory'sSINGLE_ANSWERread. An instance whose value is in its rows (rather than in a synthesized sentence over them) had no way to say so.read_modeexposes the SDK's existingsingle-answer/raw-tables/xresponsechoice.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_resultsalongside the combinedreader_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, notsingle_answer), soReadMode(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 existingread_mode: raw_tableskeeps 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-answercarries ananswerenvelope to unwrap; the other modes return tables, rendered as JSON. This matters: shape-sniffing would descend intoraw-tablesrows and mistake a data row'sanswercolumn 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
erroron 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_resultwhen 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_modedefaults tosingle-answer, so anyone with xmemory configured and no explicitread_modesees byte-identical recall output. The whole feature is opt-in.The one visible change: the recall section label drops
synthesized answerfor a plain[xmemory], since under the structured modes the payload is rows, not an answer.Known sharp edge (not addressed here)
memory_recallclips 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 intoraw-tablesthe JSON can be cut mid-token and reach the model malformed: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-answerpayload:raw-tablesrows are not mined foranswerkeysensure_ascii=False)recall_answerNote: the repo's documented
pytest tests/ -vdoes not currently run —uv run pytestfails to resolve becausepyproject.tomldeclaresrequires-python = ">=3.12"whilememu-py==1.4.0requires>=3.13. That reproduces on cleanmainand is unrelated to this PR, so I left it alone;requires-python = ">=3.13"fixes it if someone wants to pick it up.