From 6cb28566ec7dd48f5abc6c418c858013769c94ff Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 14 May 2026 13:12:59 -0700 Subject: [PATCH] =?UTF-8?q?generate/stream:=20fix=20agenerate()=20?= =?UTF-8?q?=E2=80=94=20add=20vault=20recall=20parity=20with=20generate()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agenerate() skipped _recall_state() entirely, meaning async streaming responses were disconnected from session memory. This patch brings agenerate() to full parity with the synchronous path: - Accepts vault and recall_top_k parameters (default 3, matching generate()) - Calls _recall_state(_voiced_state(current, persona), vault, recall_top_k) at each step before nearest-node selection - Does not add stop_nodes or salience (those remain sync-only for now; the core correctness gap is vault recall) The async return value is still token-by-token via yield. Callers that want final_state should use the synchronous path or wrap in a collector. --- generate/stream.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/generate/stream.py b/generate/stream.py index 393157d9..4496ffbe 100644 --- a/generate/stream.py +++ b/generate/stream.py @@ -296,14 +296,20 @@ async def agenerate( vocab, persona, max_tokens: int = 128, + vault=None, + recall_top_k: int = 3, ): """ Async streaming version — yields one token at a time. - The caller must await the generator and can retrieve final_state - by calling .athrow() or by consuming the StopAsyncIteration value. - For the final state, prefer the synchronous generate() path or - wrap in an async collector that reads the return value. + Maintains parity with the synchronous generate() path: + - Persona motor applied via _voiced_state() every step + - Vault recall fed back into field via _recall_state() every step + - Recent-node and stop-node exclusion applied + + The caller receives tokens as they are emitted. For the full + GenerationResult (final_state, trajectory), use the synchronous + generate() path or wrap this generator in an async collector. Yields: str (one token per iteration) """ @@ -315,7 +321,7 @@ async def agenerate( if token in {vocab.get_word_at(i) for i in range(len(vocab))} ) for _ in range(max_tokens): - current = _voiced_state(current, persona) + current = _recall_state(_voiced_state(current, persona), vault, recall_top_k) word, word_idx = _nearest_next( vocab, current.F,