Skip to content

feat(vector): auto-index observations into vector store | 向量索引自动集成#228

Open
mechanic-Q wants to merge 1 commit into
rohitg00:mainfrom
mechanic-Q:feature/vector-auto-index
Open

feat(vector): auto-index observations into vector store | 向量索引自动集成#228
mechanic-Q wants to merge 1 commit into
rohitg00:mainfrom
mechanic-Q:feature/vector-auto-index

Conversation

@mechanic-Q
Copy link
Copy Markdown

@mechanic-Q mechanic-Q commented May 2, 2026

Summary | 概述

Automatically add compressed observations to the VectorIndex during the observe lifecycle, enabling hybrid BM25+Vector search without manual index building.

在观察生命周期中自动将压缩记忆添加到向量索引,实现 BM25+向量混合搜索。

Motivation | 动机

The VectorIndex and EmbeddingProvider infrastructure exists but observations were only added to the BM25 index. The vector index remained empty unless manually populated. This meant the hybrid search (BM25 + Vector RRF fusion) couldn't leverage semantic similarity — only BM25 keyword matching was active. For non-English content (Chinese, multilingual), BM25 alone is insufficient because the tokenizer can't handle CJK text well. Vector search bridges this gap.

向量索引基础设施已存在,但观察结果仅被添加到 BM25 索引,向量索引保持为空。混合搜索无法利用语义相似度,仅靠 BM25 关键词匹配。对于中文等非英文内容,纯 BM25 效果差,向量搜索可以弥补这一差距。

Changes | 改动

  • src/functions/observe.ts: After synthetic compression and BM25 indexing, auto-embed the narrative and add to vector index
  • src/index.ts: Pass vectorIndex and embeddingProvider to registerObserveFunction
  • Auto-indexing runs within try-catch — embedding failures are logged but don't block observation capture
  • Only activates when an embedding provider is configured (opt-in by setting AGENTMEMORY_LOCAL_EMBEDDING_MODEL or cloud API keys)

Combined with PR #223 (configurable embedding)

# Enable multilingual hybrid search (BM25 + Vector)
AGENTMEMORY_LOCAL_EMBEDDING_MODEL=Xenova/bge-m3

This combination gives agentmemory full Chinese/multilingual semantic search capability — BM25 handles exact terms, vector handles meaning.

Backwards Compatibility | 向后兼容

New params are optional. When vectorIndex or embeddingProvider is null/undefined (default), the auto-indexing is skipped. No behavior change for existing deployments.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added optional vector indexing and embedding support for observations, enabling automatic metadata indexing when configured
    • Enhanced with external control over vector auto-indexing functionality
    • Improved resilience through graceful error handling that prevents vector operation failures from disrupting observation workflows

When an EmbeddingProvider is configured, automatically add compressed
observations to the VectorIndex during the observe lifecycle (synthetic
compression path). This enables hybrid BM25+Vector search without
requiring manual index building.

The auto-indexing runs after BM25 indexing within a try-catch so
embedding failures don't block observation capture. Combined with
configurable embedding model (AGENTMEMORY_LOCAL_EMBEDDING_MODEL),
users can enable multilingual vector search by setting the model
to bge-m3 or multilingual-e5.

Passes vectorIndex and embeddingProvider as optional params to
registerObserveFunction — fully backwards compatible.

当配置了嵌入模型时,自动将压缩后的观察结果添加到向量索引,
实现 BM25+向量混合搜索。嵌入失败不影响观察捕获。

配合可配置嵌入模型使用,设置 AGENTMEMORY_LOCAL_EMBEDDING_MODEL=bge-m3
即可启用中文等多语言语义搜索。
@vercel
Copy link
Copy Markdown

vercel Bot commented May 2, 2026

@mechanic-Q is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Updated registerObserveFunction to accept optional vectorIndex and embeddingProvider parameters. During non-LLM compression, the function now attempts to embed and index a combined "title + narrative" string, with embedding failures logged but not blocking the observation flow.

Changes

Vector Indexing Integration

Layer / File(s) Summary
Type Imports & Signature
src/functions/observe.ts (lines 2, 11, 39–43)
Added imports for EmbeddingProvider and VectorIndex. Updated registerObserveFunction signature to accept optional vectorIndex and embeddingProvider parameters.
Embedding & Indexing Logic
src/functions/observe.ts (lines 244–255)
After synthetic compression creation, conditionally embeds a combined "title + narrative" string and adds the vector to the index when both providers are available. Embedding/indexing errors are caught and logged without aborting the observation flow.
Initialization Wiring
src/index.ts (line 184)
Updated registerObserveFunction call to pass vectorIndex and embeddingProvider arguments during function registration.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through vectors bright, 🐰
Embedding titles in the night,
If indexing fails, we warn but stay,
Observations flow the rabbit's way! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(vector): auto-index observations into vector store' clearly and specifically describes the main change: adding automatic vector indexing of observations. It is concise, directly related to the primary feature being added, and follows conventional commit format.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 4/8 reviews remaining, refill in 29 minutes and 56 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/observe.ts`:
- Around line 244-255: The current code awaits embeddingProvider.embed(...)
inside the observe hook which blocks the response and stream triggers; change
this to a fire-and-forget pattern by removing the await and chaining a promise
handler: call embeddingProvider.embed(narrative).then(vec =>
vectorIndex.add(obsId, payload.sessionId, vec)).catch(err => logger.warn("Vector
auto-index failed for observation", { obsId, error: err instanceof Error ?
err.message : String(err) })); keep the surrounding conditional (vectorIndex &&
embeddingProvider) and do not reintroduce await so embedding work runs
asynchronously and does not block stream::set or the hook response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aad77881-b4cf-4f03-8c28-df9a87ed4403

📥 Commits

Reviewing files that changed from the base of the PR and between 94fc119 and e6a34d0.

📒 Files selected for processing (2)
  • src/functions/observe.ts
  • src/index.ts

Comment thread src/functions/observe.ts
Comment on lines +244 to +255
if (vectorIndex && embeddingProvider) {
try {
const narrative = (synthetic.title || "") + " " + (synthetic.narrative || "");
const vec = await embeddingProvider.embed(narrative);
vectorIndex.add(obsId, payload.sessionId, vec);
} catch (err) {
logger.warn("Vector auto-index failed for observation", {
obsId,
error: err instanceof Error ? err.message : String(err),
});
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

await embeddingProvider.embed() is on the critical path — blocks the hook response and all downstream stream triggers.

The observe hook returns only after the embedding completes (line 247). ML inference (e.g., Xenova/bge-m3) adds 100 ms–2 s to every tool-use observation for anyone who opts into an embedding provider. This also contradicts the PR's stated design ("failures are logged and do not prevent observation capture"), because successful embeddings still block the hook response and the stream::set triggers at lines 256–277.

The existing pattern for async ML work in this same function is fire-and-forget via sdk.triggerVoid (see the vision-embed dispatch at lines 146–152). The simplest fix consistent with that pattern is dropping the await and using a chained .catch() so embedding errors are still swallowed:

⚡ Proposed fix — fire-and-forget embedding (non-blocking)
-          if (vectorIndex && embeddingProvider) {
-            try {
-              const narrative = (synthetic.title || "") + " " + (synthetic.narrative || "");
-              const vec = await embeddingProvider.embed(narrative);
-              vectorIndex.add(obsId, payload.sessionId, vec);
-            } catch (err) {
-              logger.warn("Vector auto-index failed for observation", {
-                obsId,
-                error: err instanceof Error ? err.message : String(err),
-              });
-            }
-          }
+          if (vectorIndex && embeddingProvider) {
+            const narrative = (synthetic.title || "") + " " + (synthetic.narrative || "");
+            embeddingProvider.embed(narrative)
+              .then((vec) => vectorIndex.add(obsId, payload.sessionId, vec))
+              .catch((err) =>
+                logger.warn("Vector auto-index failed for observation", {
+                  obsId,
+                  error: err instanceof Error ? err.message : String(err),
+                }),
+              );
+          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (vectorIndex && embeddingProvider) {
try {
const narrative = (synthetic.title || "") + " " + (synthetic.narrative || "");
const vec = await embeddingProvider.embed(narrative);
vectorIndex.add(obsId, payload.sessionId, vec);
} catch (err) {
logger.warn("Vector auto-index failed for observation", {
obsId,
error: err instanceof Error ? err.message : String(err),
});
}
}
if (vectorIndex && embeddingProvider) {
const narrative = (synthetic.title || "") + " " + (synthetic.narrative || "");
embeddingProvider.embed(narrative)
.then((vec) => vectorIndex.add(obsId, payload.sessionId, vec))
.catch((err) =>
logger.warn("Vector auto-index failed for observation", {
obsId,
error: err instanceof Error ? err.message : String(err),
}),
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/observe.ts` around lines 244 - 255, The current code awaits
embeddingProvider.embed(...) inside the observe hook which blocks the response
and stream triggers; change this to a fire-and-forget pattern by removing the
await and chaining a promise handler: call
embeddingProvider.embed(narrative).then(vec => vectorIndex.add(obsId,
payload.sessionId, vec)).catch(err => logger.warn("Vector auto-index failed for
observation", { obsId, error: err instanceof Error ? err.message : String(err)
})); keep the surrounding conditional (vectorIndex && embeddingProvider) and do
not reintroduce await so embedding work runs asynchronously and does not block
stream::set or the hook response.

@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, When AGENTMEMORY_AUTO_COMPRESS=true, mem::observe triggers mem::compress and does not run the new vectorIndex.add() code path, so the vector index stays empty even if an EmbeddingProvider is configured.

Severity: action required | Category: correctness

How to fix: Index vectors in mem::compress

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

With AGENTMEMORY_AUTO_COMPRESS=true, mem::observe delegates compression to mem::compress, but vectors are only added during the synthetic compression path. This leaves the VectorIndex empty in the LLM compression mode.

Issue Context

  • Vector indexing currently happens only for buildSyntheticCompression(raw) output.
  • mem::compress persists the compressed observation and updates BM25 (getSearchIndex().add(compressed)), but does not embed/store vectors.

Fix Focus Areas

  • src/functions/observe.ts[222-256]
  • src/functions/compress.ts[170-178]

What to change

  • Add optional vectorIndex + embeddingProvider plumb-through to registerCompressFunction and embed compressed.title + ' ' + compressed.narrative (or similar) inside mem::compress after KV set/BM25 add.
  • Ensure failures are best-effort (log warn, do not fail compression).
  • Keep behavior consistent with synthetic path (same obsId/sessionId used).

Spotted by Qodo code review - free for open-source projects.

@rohitg00
Copy link
Copy Markdown
Owner

rohitg00 commented May 8, 2026

Reviewed — this fixes a real gap. Verified locally: vectorIndex.add(...) was only called from IndexPersistence.restoreFrom() on startup, never from the live observe pipeline. So unless an instance had been running long enough for persistence to kick in (or had imported a snapshot), the in-memory vector index was empty even though embeddingProvider was wired. New observations went into BM25 only, defeating the point of the hybrid search story.

What I like:

  • Threading vectorIndex + embeddingProvider through registerObserveFunction(...) — explicit dependencies, easy to see the data flow at the call site in index.ts.
  • The catch block logs and continues. Embedding-provider failures shouldn't kill an observation; BM25 still indexes it.
  • Built from synthetic.title + " " + synthetic.narrative — same shape the persistence layer uses, so the embedding semantics stay consistent across live and restored vectors.

Two small notes:

  • The embed call is awaited inline. On a busy session with a heavy embedding provider (OpenAI / Voyage), this will add latency to every PostToolUse observation. Worth flagging in the PR body so we know when to revisit. The non-blocking alternative would be void embeddingProvider.embed(...).then(vec => vectorIndex.add(obsId, payload.sessionId, vec)), but that risks ordering issues if observations fire faster than embeds resolve. Awaiting is the safer default.
  • IndexPersistence.save() should now also be picking up these live-added vectors — please confirm a save-then-reload cycle preserves them. (If save() reads from vectorIndex.serialize() and that's the same instance, this is automatic, but worth a one-line check.)

No blockers. Approving — holding for @rohitg00 to land.

rohitg00 added a commit that referenced this pull request May 9, 2026
) (#258)

memory_save returned 200 with a valid memory ID, but every subsequent
memory_smart_search and memory_recall returned empty results. Direct
REST /agentmemory/smart-search showed the same — confirming the bug was
not in the MCP layer.

Root cause: mem::remember in src/functions/remember.ts wrote the new
Memory to KV.memories but never called getSearchIndex().add(). The same
pattern that #228 fixed for vectors-on-observe was missing here for
BM25-on-remember. Compounded by rebuildIndex() walking only sessions/
observations and skipping KV.memories, so even a restart-rebuild
couldn't recover indexed memories.

Three changes:

1. src/functions/remember.ts — synthesize a CompressedObservation-shaped
   record from the saved Memory (title + content + concepts + files) and
   add it to BM25 right after kv.set(). Wrapped in try/catch so an
   indexing hiccup never blocks the durable save.

2. src/functions/search.ts — rebuildIndex() now walks KV.memories before
   sessions/observations, so a fresh rebuild covers the full corpus.
   Skips memory.isLatest === false so superseded entries don't pollute
   results.

3. src/index.ts — startup backfill for users upgrading from <0.9.5: when
   the persisted BM25 is non-empty (no rebuild triggered) but legacy
   memories were never indexed, walk KV.memories and add anything
   missing. SearchIndex.has(id) is the new idempotency gate.
   indexPersistence.scheduleSave() persists the augmented index.

Tests in test/remember-bm25-index.test.ts cover:
- SearchIndex.has() before/after add
- A saved memory is findable by keyword (the case the bug broke)
- The exact repro from the issue (mem_moy3u6ua_..., query "BM25 test")
- Concept-only matches (no title/content overlap)

Out of scope (file as follow-up):
- Removing superseded memories from BM25 when mem::cascade-update fires.
  Currently relies on isLatest filter at result time; not perfect but
  doesn't regress recall.
- Memory restore via IndexPersistence (covered transparently by the
  startup backfill, but a dedicated round-trip test would be tighter).

Reported by @Nizar-BenHamida with full repro + log capture.
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.

3 participants