@chendpoc/pi-memory 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -60
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/preflight.d.ts +8 -0
- package/dist/config/preflight.d.ts.map +1 -0
- package/dist/config/preflight.js +22 -0
- package/dist/config/preflight.js.map +1 -0
- package/dist/constants/env.d.ts +3 -0
- package/dist/constants/env.d.ts.map +1 -1
- package/dist/constants/env.js +3 -0
- package/dist/constants/env.js.map +1 -1
- package/dist/constants/preflight.d.ts +7 -0
- package/dist/constants/preflight.d.ts.map +1 -1
- package/dist/constants/preflight.js +7 -0
- package/dist/constants/preflight.js.map +1 -1
- package/dist/constants/timing.d.ts +2 -0
- package/dist/constants/timing.d.ts.map +1 -1
- package/dist/constants/timing.js +2 -0
- package/dist/constants/timing.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +21 -2
- package/dist/pi-extension.js.map +1 -1
- package/dist/preflight/episodic.d.ts +7 -1
- package/dist/preflight/episodic.d.ts.map +1 -1
- package/dist/preflight/episodic.js +20 -4
- package/dist/preflight/episodic.js.map +1 -1
- package/dist/preflight/intentCache.d.ts +13 -0
- package/dist/preflight/intentCache.d.ts.map +1 -0
- package/dist/preflight/intentCache.js +37 -0
- package/dist/preflight/intentCache.js.map +1 -0
- package/dist/preflight/queryIntent.d.ts +14 -5
- package/dist/preflight/queryIntent.d.ts.map +1 -1
- package/dist/preflight/queryIntent.js +27 -11
- package/dist/preflight/queryIntent.js.map +1 -1
- package/dist/sidecar/warmup.d.ts +6 -0
- package/dist/sidecar/warmup.d.ts.map +1 -0
- package/dist/sidecar/warmup.js +39 -0
- package/dist/sidecar/warmup.js.map +1 -0
- package/doc/README-zh.md +365 -0
- package/doc/ROADMAP-zh.md +52 -0
- package/doc/ROADMAP.md +52 -0
- package/package.json +4 -1
- package/src/config/index.ts +1 -0
- package/src/config/preflight.ts +34 -0
- package/src/constants/env.ts +3 -0
- package/src/constants/preflight.ts +9 -0
- package/src/constants/timing.ts +2 -0
- package/src/pi-extension.ts +22 -2
- package/src/preflight/episodic.ts +28 -4
- package/src/preflight/intentCache.ts +44 -0
- package/src/preflight/queryIntent.ts +46 -12
- package/src/sidecar/warmup.ts +50 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPreflightRuntimeConfig } from "../config/preflight.js";
|
|
1
2
|
import { resolvePreflightBudget } from "../config/preflightBudget.js";
|
|
2
3
|
import type { LlmClient } from "../adapters/llm/types.js";
|
|
3
4
|
import { debugMemory } from "../utils/debugLog.js";
|
|
@@ -20,7 +21,13 @@ export type EpisodicPreflightOptions = {
|
|
|
20
21
|
agentDir: string;
|
|
21
22
|
store: MemoryStore;
|
|
22
23
|
llm?: LlmClient | null;
|
|
24
|
+
/** Force episodic gate (first turn); does not force helper LLM. */
|
|
25
|
+
forceEpisodic?: boolean;
|
|
26
|
+
/** Force helper LLM intent extraction. */
|
|
27
|
+
forceIntent?: boolean;
|
|
28
|
+
/** @deprecated Use forceEpisodic */
|
|
23
29
|
force?: boolean;
|
|
30
|
+
sessionId?: string;
|
|
24
31
|
budgetMs?: number;
|
|
25
32
|
signal?: AbortSignal;
|
|
26
33
|
onProgress?: (message: string) => void;
|
|
@@ -67,16 +74,23 @@ async function withTimeout<T>(
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/**
|
|
70
|
-
* Fail-silent episodic preflight: QueryIntent (
|
|
77
|
+
* Fail-silent episodic preflight: QueryIntent (optional) → buildRetrievalQuery → sidecar.query → fallback.
|
|
71
78
|
*/
|
|
72
79
|
export async function runEpisodicPreflight(
|
|
73
80
|
userInput: string,
|
|
74
81
|
options: EpisodicPreflightOptions,
|
|
75
82
|
): Promise<EpisodicPreflightResult | null> {
|
|
76
83
|
const startedAt = nowMs();
|
|
84
|
+
const forceEpisodic = options.forceEpisodic ?? options.force ?? false;
|
|
85
|
+
const runtime = readPreflightRuntimeConfig();
|
|
77
86
|
|
|
78
87
|
try {
|
|
79
|
-
if (
|
|
88
|
+
if (await options.store.isEmpty()) {
|
|
89
|
+
debugMemory("preflight", "skipped", { reason: "empty" });
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!shouldRunEpisodicPreflight(userInput, forceEpisodic)) {
|
|
80
94
|
debugMemory("preflight", "skipped", { reason: "gate" });
|
|
81
95
|
return null;
|
|
82
96
|
}
|
|
@@ -88,15 +102,23 @@ export async function runEpisodicPreflight(
|
|
|
88
102
|
const intentStartedAt = nowMs();
|
|
89
103
|
const intentBudget = Math.min(budget.intentMs, remainingMs(deadline));
|
|
90
104
|
let intent;
|
|
105
|
+
let intentSkipped = false;
|
|
106
|
+
let intentCacheHit = false;
|
|
107
|
+
|
|
91
108
|
try {
|
|
92
|
-
|
|
109
|
+
const extracted = await withTimeout(
|
|
93
110
|
extractQueryIntent(userInput, options.llm ?? null, {
|
|
94
|
-
|
|
111
|
+
forceIntent: options.forceIntent,
|
|
95
112
|
signal: options.signal,
|
|
113
|
+
sessionId: options.sessionId,
|
|
114
|
+
intentCache: runtime.intentCache,
|
|
96
115
|
}),
|
|
97
116
|
intentBudget,
|
|
98
117
|
options.signal,
|
|
99
118
|
);
|
|
119
|
+
intent = extracted.intent;
|
|
120
|
+
intentSkipped = extracted.skipped;
|
|
121
|
+
intentCacheHit = extracted.cacheHit;
|
|
100
122
|
} catch {
|
|
101
123
|
intent = { raw_query: userInput.trim() };
|
|
102
124
|
}
|
|
@@ -143,6 +165,8 @@ export async function runEpisodicPreflight(
|
|
|
143
165
|
|
|
144
166
|
debugMemory("preflight", "recall", {
|
|
145
167
|
intent_ms: intentMs,
|
|
168
|
+
intent_skipped: intentSkipped,
|
|
169
|
+
intent_cache_hit: intentCacheHit,
|
|
146
170
|
sidecar_ms: sidecarMs,
|
|
147
171
|
total_ms: nowMs() - startedAt,
|
|
148
172
|
cache_hit: cacheHit,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LRUCache } from "lru-cache";
|
|
2
|
+
|
|
3
|
+
import { INTENT_CACHE_MAX_ENTRIES } from "../constants/preflight.js";
|
|
4
|
+
import type { QueryIntent } from "./queryIntent.js";
|
|
5
|
+
|
|
6
|
+
function normalizeInput(input: string): string {
|
|
7
|
+
return input.trim().replace(/\s+/g, " ").toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cacheKey(sessionId: string, userInput: string): string {
|
|
11
|
+
return `${sessionId}\0${normalizeInput(userInput)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class QueryIntentCache {
|
|
15
|
+
private readonly lru = new LRUCache<string, QueryIntent>({ max: INTENT_CACHE_MAX_ENTRIES });
|
|
16
|
+
|
|
17
|
+
get(sessionId: string | undefined, userInput: string): QueryIntent | null {
|
|
18
|
+
if (!sessionId) return null;
|
|
19
|
+
return this.lru.get(cacheKey(sessionId, userInput)) ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
set(sessionId: string | undefined, userInput: string, intent: QueryIntent): void {
|
|
23
|
+
if (!sessionId) return;
|
|
24
|
+
this.lru.set(cacheKey(sessionId, userInput), intent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
clearSession(sessionId: string): void {
|
|
28
|
+
const prefix = `${sessionId}\0`;
|
|
29
|
+
for (const key of this.lru.keys()) {
|
|
30
|
+
if (key.startsWith(prefix)) this.lru.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @internal test hook */
|
|
35
|
+
resetForTests(): void {
|
|
36
|
+
this.lru.clear();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const queryIntentCache = new QueryIntentCache();
|
|
41
|
+
|
|
42
|
+
export function resetQueryIntentCacheForTests(): void {
|
|
43
|
+
queryIntentCache.resetForTests();
|
|
44
|
+
}
|
|
@@ -2,12 +2,15 @@ import compact from "lodash/compact.js";
|
|
|
2
2
|
import { Type, type Static } from "typebox";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
4
|
|
|
5
|
+
import { readPreflightRuntimeConfig } from "../config/preflight.js";
|
|
5
6
|
import {
|
|
7
|
+
DEFAULT_INTENT_RETRIES,
|
|
6
8
|
MEMORY_CUE_RE,
|
|
7
9
|
PREFLIGHT_EXTRACT_MIN_LENGTH,
|
|
8
10
|
PREFLIGHT_SKIP_MIN_LENGTH,
|
|
9
11
|
} from "../constants/preflight.js";
|
|
10
12
|
import type { LlmClient } from "../adapters/llm/types.js";
|
|
13
|
+
import { queryIntentCache } from "./intentCache.js";
|
|
11
14
|
|
|
12
15
|
/** Structured intent from helper LLM — simpler than crafting a search query directly. */
|
|
13
16
|
export const QueryIntentSchema = Type.Object(
|
|
@@ -35,17 +38,17 @@ Use raw_query when the whole message should be searched verbatim.
|
|
|
35
38
|
User message:
|
|
36
39
|
`;
|
|
37
40
|
|
|
38
|
-
export function shouldRunEpisodicPreflight(query: string,
|
|
41
|
+
export function shouldRunEpisodicPreflight(query: string, forceEpisodic = false): boolean {
|
|
39
42
|
const trimmed = query.trim();
|
|
40
43
|
if (!trimmed) return false;
|
|
41
|
-
if (
|
|
44
|
+
if (forceEpisodic) return true;
|
|
42
45
|
if (trimmed.startsWith("/")) return false;
|
|
43
46
|
if (trimmed.length < PREFLIGHT_SKIP_MIN_LENGTH && !MEMORY_CUE_RE.test(trimmed)) return false;
|
|
44
47
|
return MEMORY_CUE_RE.test(trimmed) || trimmed.length >= PREFLIGHT_EXTRACT_MIN_LENGTH;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function shouldExtractIntent(query: string,
|
|
48
|
-
if (
|
|
50
|
+
export function shouldExtractIntent(query: string, forceIntent = false): boolean {
|
|
51
|
+
if (forceIntent) return true;
|
|
49
52
|
if (query.trim().length >= PREFLIGHT_EXTRACT_MIN_LENGTH) return true;
|
|
50
53
|
return MEMORY_CUE_RE.test(query);
|
|
51
54
|
}
|
|
@@ -69,24 +72,55 @@ function extractJsonObject(text: string): unknown {
|
|
|
69
72
|
return JSON.parse(text.slice(start, end + 1));
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
export type ExtractQueryIntentOptions = {
|
|
76
|
+
forceIntent?: boolean;
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
intentCache?: boolean;
|
|
80
|
+
intentRetries?: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type ExtractQueryIntentResult = {
|
|
84
|
+
intent: QueryIntent;
|
|
85
|
+
skipped: boolean;
|
|
86
|
+
cacheHit: boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
72
89
|
export async function extractQueryIntent(
|
|
73
90
|
userInput: string,
|
|
74
91
|
llm: LlmClient | null,
|
|
75
|
-
options:
|
|
76
|
-
): Promise<
|
|
92
|
+
options: ExtractQueryIntentOptions = {},
|
|
93
|
+
): Promise<ExtractQueryIntentResult> {
|
|
77
94
|
const fallback: QueryIntent = { raw_query: userInput.trim() };
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
const runtime = readPreflightRuntimeConfig();
|
|
96
|
+
const useCache = options.intentCache ?? runtime.intentCache;
|
|
97
|
+
|
|
98
|
+
if (useCache && options.sessionId) {
|
|
99
|
+
const cached = queryIntentCache.get(options.sessionId, userInput);
|
|
100
|
+
if (cached) {
|
|
101
|
+
return { intent: cached, skipped: false, cacheHit: true };
|
|
102
|
+
}
|
|
80
103
|
}
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
if (!llm || !shouldExtractIntent(userInput, options.forceIntent)) {
|
|
106
|
+
return { intent: fallback, skipped: true, cacheHit: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const retries = options.intentRetries ?? runtime.intentRetries ?? DEFAULT_INTENT_RETRIES;
|
|
110
|
+
const maxAttempts = Math.max(1, retries + 1);
|
|
111
|
+
|
|
112
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
83
113
|
try {
|
|
84
114
|
const raw = await llm.complete(`${INTENT_PROMPT}${userInput}`, options.signal);
|
|
85
|
-
|
|
115
|
+
const intent = parseQueryIntent(extractJsonObject(raw));
|
|
116
|
+
if (useCache && options.sessionId) {
|
|
117
|
+
queryIntentCache.set(options.sessionId, userInput, intent);
|
|
118
|
+
}
|
|
119
|
+
return { intent, skipped: false, cacheHit: false };
|
|
86
120
|
} catch {
|
|
87
|
-
//
|
|
121
|
+
// retry or fallback
|
|
88
122
|
}
|
|
89
123
|
}
|
|
90
124
|
|
|
91
|
-
return fallback;
|
|
125
|
+
return { intent: fallback, skipped: false, cacheHit: false };
|
|
92
126
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fetchIndexStats, ping, query } from "./client.js";
|
|
2
|
+
import { SIDECAR_WARMUP_QUERY_TIMEOUT_MS } from "../constants/timing.js";
|
|
3
|
+
import { debugMemory } from "../utils/debugLog.js";
|
|
4
|
+
import { nowMs } from "../utils/time.js";
|
|
5
|
+
|
|
6
|
+
export type WarmSidecarOptions = {
|
|
7
|
+
queryTimeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Pre-open sidecar DB and optionally smoke-test embed + vec paths. Fail-silent. */
|
|
11
|
+
export async function warmSidecar(
|
|
12
|
+
socketPath: string,
|
|
13
|
+
options: WarmSidecarOptions = {},
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const startedAt = nowMs();
|
|
16
|
+
|
|
17
|
+
if (!(await ping(socketPath))) {
|
|
18
|
+
debugMemory("sidecar", "warm", { ok: false, reason: "ping" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statsStartedAt = nowMs();
|
|
23
|
+
const statsResult = await fetchIndexStats(socketPath);
|
|
24
|
+
const statsMs = nowMs() - statsStartedAt;
|
|
25
|
+
|
|
26
|
+
if ("error" in statsResult) {
|
|
27
|
+
debugMemory("sidecar", "warm", { ok: false, stats_ms: statsMs, reason: "stats" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let queryMs = 0;
|
|
32
|
+
if (statsResult.stats.chunk_count > 0) {
|
|
33
|
+
const queryStartedAt = nowMs();
|
|
34
|
+
const queryTimeoutMs = options.queryTimeoutMs ?? SIDECAR_WARMUP_QUERY_TIMEOUT_MS;
|
|
35
|
+
try {
|
|
36
|
+
await query(socketPath, ".", queryTimeoutMs);
|
|
37
|
+
} catch {
|
|
38
|
+
// warm query is best-effort
|
|
39
|
+
}
|
|
40
|
+
queryMs = nowMs() - queryStartedAt;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
debugMemory("sidecar", "warm", {
|
|
44
|
+
ok: true,
|
|
45
|
+
stats_ms: statsMs,
|
|
46
|
+
query_ms: queryMs,
|
|
47
|
+
chunks: statsResult.stats.chunk_count,
|
|
48
|
+
total_ms: nowMs() - startedAt,
|
|
49
|
+
});
|
|
50
|
+
}
|