|
5 | 5 | jsonResult, |
6 | 6 | readNumberParam, |
7 | 7 | readStringParam, |
| 8 | + type MemoryCorpusSearchResult, |
8 | 9 | type OpenClawConfig, |
9 | 10 | } from "openclaw/plugin-sdk/memory-core-host-runtime-core"; |
10 | 11 | import type { |
@@ -35,6 +36,50 @@ import { |
35 | 36 | searchMemoryCorpusSupplements, |
36 | 37 | } from "./tools.shared.js"; |
37 | 38 |
|
| 39 | +type MemorySearchToolResult = |
| 40 | + | (Record<string, unknown> & { corpus: "memory"; score: number; path: string }) |
| 41 | + | MemoryCorpusSearchResult; |
| 42 | + |
| 43 | +function sortMemorySearchToolResults<T extends { score: number; path: string }>(results: T[]): T[] { |
| 44 | + return results.toSorted((left, right) => { |
| 45 | + if (left.score !== right.score) { |
| 46 | + return right.score - left.score; |
| 47 | + } |
| 48 | + return left.path.localeCompare(right.path); |
| 49 | + }); |
| 50 | +} |
| 51 | + |
| 52 | +function mergeMemorySearchCorpusResults(params: { |
| 53 | + memoryResults: MemorySearchToolResult[]; |
| 54 | + supplementResults: MemorySearchToolResult[]; |
| 55 | + maxResults: number; |
| 56 | + balanceCorpora: boolean; |
| 57 | +}): MemorySearchToolResult[] { |
| 58 | + const memoryResults = sortMemorySearchToolResults(params.memoryResults); |
| 59 | + const supplementResults = sortMemorySearchToolResults(params.supplementResults); |
| 60 | + if (!params.balanceCorpora || memoryResults.length === 0 || supplementResults.length === 0) { |
| 61 | + return sortMemorySearchToolResults([...memoryResults, ...supplementResults]).slice( |
| 62 | + 0, |
| 63 | + params.maxResults, |
| 64 | + ); |
| 65 | + } |
| 66 | + |
| 67 | + const perCorpusCap = Math.ceil(params.maxResults / 2); |
| 68 | + const selectedMemory = memoryResults.slice(0, perCorpusCap); |
| 69 | + const selectedSupplements = supplementResults.slice(0, perCorpusCap); |
| 70 | + const selected = [...selectedMemory, ...selectedSupplements]; |
| 71 | + if (selected.length < params.maxResults) { |
| 72 | + selected.push( |
| 73 | + ...sortMemorySearchToolResults([ |
| 74 | + ...memoryResults.slice(selectedMemory.length), |
| 75 | + ...supplementResults.slice(selectedSupplements.length), |
| 76 | + ]).slice(0, params.maxResults - selected.length), |
| 77 | + ); |
| 78 | + } |
| 79 | + |
| 80 | + return sortMemorySearchToolResults(selected).slice(0, params.maxResults); |
| 81 | +} |
| 82 | + |
38 | 83 | function buildRecallKey( |
39 | 84 | result: Pick<MemorySearchResult, "source" | "path" | "startLine" | "endLine">, |
40 | 85 | ): string { |
@@ -319,14 +364,15 @@ export function createMemorySearchTool(options: { |
319 | 364 | corpus: requestedCorpus, |
320 | 365 | }) |
321 | 366 | : []; |
322 | | - const results = [...surfacedMemoryResults, ...supplementResults] |
323 | | - .toSorted((left, right) => { |
324 | | - if (left.score !== right.score) { |
325 | | - return right.score - left.score; |
326 | | - } |
327 | | - return left.path.localeCompare(right.path); |
328 | | - }) |
329 | | - .slice(0, Math.max(1, maxResults ?? 10)); |
| 367 | + // Wiki and memory scores use incomparable scales, so corpus=all first |
| 368 | + // balances candidate selection and then backfills any unused slots. |
| 369 | + const effectiveMax = Math.max(1, maxResults ?? 10); |
| 370 | + const results = mergeMemorySearchCorpusResults({ |
| 371 | + memoryResults: surfacedMemoryResults, |
| 372 | + supplementResults, |
| 373 | + maxResults: effectiveMax, |
| 374 | + balanceCorpora: requestedCorpus === "all", |
| 375 | + }); |
330 | 376 | return jsonResult({ |
331 | 377 | results, |
332 | 378 | provider, |
|
0 commit comments