From 5257b96eb1a98f77c9a78a9fa94b03eb70a1f0ce Mon Sep 17 00:00:00 2001 From: mayank Date: Sat, 11 Jul 2026 11:43:53 +0530 Subject: [PATCH] fix(memory-agent): resolve traceability chain, parser type-safety, config compatibility, benchmark, and searchL1Hybrid bugs --- src/adapters/standalone/llm-runner.ts | 5 +-- src/core/hooks/auto-recall.ts | 2 +- src/offload/benchmark-token-estimate.ts | 2 +- src/offload/index.ts | 6 ++- src/offload/l3-helpers.ts | 2 +- src/offload/local-llm/llm-caller.ts | 5 +-- .../local-llm/parsers/l1-parser.test.ts | 41 +++++++++++++++++++ src/offload/local-llm/parsers/l1-parser.ts | 1 + 8 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 src/offload/local-llm/parsers/l1-parser.test.ts diff --git a/src/adapters/standalone/llm-runner.ts b/src/adapters/standalone/llm-runner.ts index cf127ca3..166cf119 100644 --- a/src/adapters/standalone/llm-runner.ts +++ b/src/adapters/standalone/llm-runner.ts @@ -19,7 +19,7 @@ import fsPromises from "node:fs/promises"; import path from "node:path"; import { generateText, tool, stepCountIs, jsonSchema } from "ai"; -import { createOpenAI } from "@ai-sdk/openai"; +import { createOpenAI, type OpenAIProviderSettings } from "@ai-sdk/openai"; import { report } from "../../core/report/reporter.js"; import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js"; import type { @@ -203,9 +203,8 @@ export class StandaloneLLMRunner implements LLMRunner { const provider = createOpenAI({ baseURL: this.config.baseUrl, apiKey: this.config.apiKey, - compatibility: "compatible", ...(this.customFetch ? { fetch: this.customFetch } : {}), - }); + } satisfies OpenAIProviderSettings); // For pure text tasks like L1 extraction, avoid exposing any tools. const tools = this.enableTools diff --git a/src/core/hooks/auto-recall.ts b/src/core/hooks/auto-recall.ts index 23c9237c..e1a96c9d 100644 --- a/src/core/hooks/auto-recall.ts +++ b/src/core/hooks/auto-recall.ts @@ -374,7 +374,7 @@ async function searchMemories( // Hybrid: if the store natively supports hybrid search (e.g. TCVDB does // server-side dense + sparse + RRF in a single API call), short-circuit // to avoid a redundant second HTTP request and a wasted local embed(). - if (vectorStore?.getCapabilities().nativeHybridSearch) { + if (vectorStore?.getCapabilities().nativeHybridSearch && typeof vectorStore.searchL1Hybrid === "function") { const tNative = performance.now(); const results = await vectorStore.searchL1Hybrid({ query: cleanText, topK: maxResults }); const nativeMs = performance.now() - tNative; diff --git a/src/offload/benchmark-token-estimate.ts b/src/offload/benchmark-token-estimate.ts index d7eae7e0..2188125d 100644 --- a/src/offload/benchmark-token-estimate.ts +++ b/src/offload/benchmark-token-estimate.ts @@ -1,7 +1,7 @@ /** * Benchmark: fastEstimateTokens vs tiktoken cl100k_base */ -import { fastEstimateTokens } from "../src/offload/fast-token-estimate.ts"; +import { fastEstimateTokens } from "./fast-token-estimate.ts"; import { getEncoding } from "js-tiktoken"; import { readFileSync, existsSync } from "fs"; import { join, dirname } from "path"; diff --git a/src/offload/index.ts b/src/offload/index.ts index 35c18bb4..c8435328 100644 --- a/src/offload/index.ts +++ b/src/offload/index.ts @@ -481,8 +481,10 @@ export function registerOffload(api: any, offloadConfig: OffloadConfig): void { stateManager._l1ChunkFailCounts.delete(chunkKey); if (resp.entries && resp.entries.length > 0) { for (const entry of resp.entries) { - if (!entry.result_ref && refByToolCallId.has(entry.tool_call_id)) { - entry.result_ref = refByToolCallId.get(entry.tool_call_id)!; + const lookupId = entry.tool_call_id; + const refId = lookupId && (refByToolCallId.get(lookupId) ?? refByToolCallId.get(normalizeToolCallIdForLookup(lookupId))); + if (!entry.result_ref && refId) { + entry.result_ref = refId; } } await appendOffloadEntries(stateManager.ctx, resp.entries, undefined, logger); diff --git a/src/offload/l3-helpers.ts b/src/offload/l3-helpers.ts index d9d08f90..816ac6aa 100644 --- a/src/offload/l3-helpers.ts +++ b/src/offload/l3-helpers.ts @@ -225,7 +225,7 @@ export function replaceWithSummary(msg: any, entry: OffloadEntry): { originalLen const summaryContent = [ `[Offloaded Tool Result | node: ${entry.node_id ?? "N/A"}]`, `Summary: ${entry.summary}`, - `result_ref: ${entry.result_ref} (read this file for full tool call and raw result)`, + `result_ref: ${entry.result_ref ?? ""} (read this file for full tool call and raw result)`, ].join("\n"); // Measure original content length diff --git a/src/offload/local-llm/llm-caller.ts b/src/offload/local-llm/llm-caller.ts index 9f61baaa..ea0acd53 100644 --- a/src/offload/local-llm/llm-caller.ts +++ b/src/offload/local-llm/llm-caller.ts @@ -5,7 +5,7 @@ * to support any OpenAI-compatible backend. */ import { generateText } from "ai"; -import { createOpenAI } from "@ai-sdk/openai"; +import { createOpenAI, type OpenAIProviderSettings } from "@ai-sdk/openai"; import { createNoThinkFetch, type DisableThinkingStrategy } from "../../utils/no-think-fetch.js"; import type { PluginLogger } from "../types.js"; @@ -63,9 +63,8 @@ export async function callLlm( const provider = createOpenAI({ baseURL: config.baseUrl, apiKey: config.apiKey, - compatibility: "compatible", ...(customFetch ? { fetch: customFetch } : {}), - }); + } satisfies OpenAIProviderSettings); try { const result = await generateText({ diff --git a/src/offload/local-llm/parsers/l1-parser.test.ts b/src/offload/local-llm/parsers/l1-parser.test.ts new file mode 100644 index 00000000..1403cf7d --- /dev/null +++ b/src/offload/local-llm/parsers/l1-parser.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { parseL1Response } from "./l1-parser.js"; + +describe("L1 Response Parser", () => { + it("should parse L1 entries correctly and ensure result_ref is present", () => { + const raw = JSON.stringify([ + { + tool_call: "read_file({path: 'foo.ts'})", + summary: "Reads content of foo.ts", + tool_call_id: "call_abc123", + timestamp: "2026-07-11T12:00:00Z", + score: 8, + }, + ]); + + const entries = parseL1Response(raw); + expect(entries).toHaveLength(1); + expect(entries[0]).toEqual({ + tool_call_id: "call_abc123", + tool_call: "read_file({path: 'foo.ts'})", + summary: "Reads content of foo.ts", + timestamp: "2026-07-11T12:00:00Z", + score: 8, + node_id: null, + result_ref: "", + }); + }); + + it("should ignore entries with empty tool_call_id", () => { + const raw = JSON.stringify([ + { + tool_call: "read_file({path: 'foo.ts'})", + summary: "Reads content of foo.ts", + timestamp: "2026-07-11T12:00:00Z", + }, + ]); + + const entries = parseL1Response(raw); + expect(entries).toHaveLength(0); + }); +}); diff --git a/src/offload/local-llm/parsers/l1-parser.ts b/src/offload/local-llm/parsers/l1-parser.ts index 4e790210..ae62be40 100644 --- a/src/offload/local-llm/parsers/l1-parser.ts +++ b/src/offload/local-llm/parsers/l1-parser.ts @@ -34,6 +34,7 @@ export function parseL1Response(raw: string): OffloadEntry[] { timestamp: item.timestamp ?? "", score: typeof item.score === "number" ? item.score : 5, node_id: null, + result_ref: "", }); }