Skip to content

fix(sona): expose find_patterns on EphemeralAgent + fix get_patterns returning empty#368

Open
shaal wants to merge 1 commit intoruvnet:mainfrom
shaal:fix/sona-ephemeral-get-patterns
Open

fix(sona): expose find_patterns on EphemeralAgent + fix get_patterns returning empty#368
shaal wants to merge 1 commit intoruvnet:mainfrom
shaal:fix/sona-ephemeral-get-patterns

Conversation

@shaal
Copy link
Copy Markdown
Contributor

@shaal shaal commented Apr 21, 2026

Fixes #367.

The bug, briefly

EphemeralAgent::get_patterns() was written as:

pub fn get_patterns(&self) -> Vec<LearnedPattern> {
    self.engine.find_patterns(&[], 0)
}

That inner call says "find me the top-0 patterns most similar to this empty query vector." Both halves are degenerate, so the engine dutifully returns Vec::new(). Every time. The method silently reports that an ephemeral agent has learned nothing, even after the agent has observed and internalised many patterns.

The underlying engine.find_patterns(query, k) and engine.get_all_patterns() both work correctly — they just weren't reachable from the EphemeralAgent public surface. Issue #367 has the longer walk-through of how this manifests downstream (stats counters keep working, but the actual pattern list is always empty — very easy to miss).

What this PR changes

Two files, 18 lines added, 1 removed.

crates/sona/src/training/federated.rs

get_patterns() now delegates to the engine's "give me everything you've learned" method:

pub fn get_patterns(&self) -> Vec<LearnedPattern> {
    self.engine.get_all_patterns()
}

…and a new public method forwards the engine's query-ranked top-k lookup so that callers who do want similarity-ranked results can ask for them directly rather than having to walk the full list and re-rank in their own code:

/// Find top-k patterns most similar to a query embedding
pub fn find_patterns(&self, query: &[f32], k: usize) -> Vec<LearnedPattern> {
    self.engine.find_patterns(query, k)
}

crates/sona/src/wasm.rs

A #[wasm_bindgen(js_name = findPatterns)] method on WasmEphemeralAgent, mirroring the existing getPatterns binding one level above it:

/// Find top-k patterns most similar to a query embedding
///
/// # Arguments
/// * `query_embedding` - Query vector as Float32Array
/// * `k` - Number of patterns to return
#[wasm_bindgen(js_name = findPatterns)]
pub fn find_patterns(&self, query_embedding: Vec<f32>, k: usize) -> JsValue {
    let patterns = self.inner.find_patterns(&query_embedding, k);
    serde_wasm_bindgen::to_value(&patterns).unwrap_or(JsValue::NULL)
}

Now JS consumers can call agent.findPatterns(queryFloat32Array, k) and receive a populated array of LearnedPattern objects.

Why surface a find_patterns(query, k) at all, not just fix get_patterns()

The narrow fix — swapping find_patterns(&[], 0) for get_all_patterns() inside get_patterns() — would already stop the silent-empty-list behaviour. But the reason the engine has a find_patterns(query, k) method in the first place is that callers often want a similarity-ranked top-k (e.g. "what are the 5 patterns closest to this new observation?") rather than the full accumulated list. Hiding that capability behind a method that always returns everything forces consumers to reimplement ranking client-side over a potentially long pattern list. Exposing find_patterns as its own method keeps get_patterns honest (returns everything) and lets the engine's existing ranked-retrieval do its job when needed. The two names now describe what they actually do.

Backwards compatibility

  • get_patterns() / getPatterns() — signature unchanged; behaviour shifts from always empty to the correct, non-empty list. Callers who were tolerating the empty-list case will still compile; they'll just start receiving real data.
  • find_patterns() / findPatterns() — brand-new names in both Rust and JS. No collisions with existing public API.

No Cargo.toml or dependency changes. No schema changes to LearnedPattern.

Verification done locally

I vendored the patched sona crate into a downstream consumer that exercises both methods from JS:

  1. Constructed a WasmEphemeralAgent and let it accumulate patterns.
  2. Called getPatterns() — previously returned []; now returns the expected populated LearnedPattern[] with matching get_stats().patterns count.
  3. Called findPatterns(queryEmbedding, 5) with a Float32Array query — returns a top-5 LearnedPattern[] in similarity order, matching what a manual cosine re-ranking of getPatterns() produces.

Open to shape adjustments

If you'd prefer a different API shape — for example deprecating get_patterns() in favour of find_patterns(query, k) entirely, or renaming, or splitting into two traits — I'm happy to reshape. This PR picks the minimum-surprise option (fix the existing method's contract + add the missing sibling). Let me know what fits the sona public-surface direction best.

…returning empty

`EphemeralAgent::get_patterns()` was calling `self.engine.find_patterns(&[], 0)` —
an empty-query / k=0 lookup that always returns `Vec::new()`. The shape of the
call makes it clear this was stub scaffolding rather than an intended behaviour.
Consumers calling `get_patterns()` silently received an empty list no matter how
many patterns the agent had accumulated.

The underlying `engine.find_patterns(query, k)` works correctly; the miss was
that it was never exposed as a public method on `EphemeralAgent`, so callers
had no way to actually query the reasoning bank for a top-k by similarity.

Changes (40 lines across two files):

1. crates/sona/src/training/federated.rs
   - `get_patterns()` now delegates to `self.engine.get_all_patterns()` and
     returns the full learned-pattern list.
   - New public method `find_patterns(query: &[f32], k: usize)` forwards to
     `self.engine.find_patterns(query, k)` so callers can do a cosine-ranked
     top-k from a query embedding.

2. crates/sona/src/wasm.rs
   - New `#[wasm_bindgen(js_name = findPatterns)]` binding on
     `WasmEphemeralAgent` that accepts a Float32Array query vector and a k,
     and returns the serialized `Vec<LearnedPattern>` via serde_wasm_bindgen
     (matches the existing `getPatterns` pattern).

No behavioural change for existing callers of `getPatterns` beyond it now
returning the non-empty list it was presumably always meant to return.

Verified by vendoring the patched crate into a downstream consumer and
confirming `findPatterns(embedding, k)` round-trips a populated array with
the expected LearnedPattern shape; `getPatterns()` also now returns a
non-empty list when the agent has accumulated patterns.
@shaal shaal force-pushed the fix/sona-ephemeral-get-patterns branch from 7145a86 to be020bc Compare April 22, 2026 02:54
@shaal shaal mentioned this pull request Apr 22, 2026
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.

EphemeralAgent::get_patterns() returns an empty list because of a stub-shaped internal call

1 participant