praana 0.5.0
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/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/praana.js +17 -0
- package/bin/pran.js +17 -0
- package/dist/app-banner.d.ts +11 -0
- package/dist/app-banner.js +161 -0
- package/dist/app-controller.d.ts +44 -0
- package/dist/app-controller.js +143 -0
- package/dist/app-identity.d.ts +18 -0
- package/dist/app-identity.js +52 -0
- package/dist/auto-compact.d.ts +16 -0
- package/dist/auto-compact.js +101 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.js +69 -0
- package/dist/compile-classic.d.ts +21 -0
- package/dist/compile-classic.js +106 -0
- package/dist/compiler.d.ts +75 -0
- package/dist/compiler.js +406 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +433 -0
- package/dist/context-engine/activity-log.d.ts +9 -0
- package/dist/context-engine/activity-log.js +109 -0
- package/dist/context-engine/artifact-store.d.ts +32 -0
- package/dist/context-engine/artifact-store.js +272 -0
- package/dist/context-engine/bm25.d.ts +3 -0
- package/dist/context-engine/bm25.js +32 -0
- package/dist/context-engine/checkpoint.d.ts +34 -0
- package/dist/context-engine/checkpoint.js +430 -0
- package/dist/context-engine/classify.d.ts +3 -0
- package/dist/context-engine/classify.js +60 -0
- package/dist/context-engine/db.d.ts +73 -0
- package/dist/context-engine/db.js +505 -0
- package/dist/context-engine/distiller.d.ts +30 -0
- package/dist/context-engine/distiller.js +67 -0
- package/dist/context-engine/engine-compiler.d.ts +23 -0
- package/dist/context-engine/engine-compiler.js +297 -0
- package/dist/context-engine/error-tracker.d.ts +21 -0
- package/dist/context-engine/error-tracker.js +74 -0
- package/dist/context-engine/event-lineage.d.ts +26 -0
- package/dist/context-engine/event-lineage.js +120 -0
- package/dist/context-engine/extraction.d.ts +26 -0
- package/dist/context-engine/extraction.js +83 -0
- package/dist/context-engine/index.d.ts +82 -0
- package/dist/context-engine/index.js +238 -0
- package/dist/context-engine/scoring.d.ts +13 -0
- package/dist/context-engine/scoring.js +47 -0
- package/dist/context-engine/state-snapshot.d.ts +8 -0
- package/dist/context-engine/state-snapshot.js +50 -0
- package/dist/context-engine/summarize.d.ts +6 -0
- package/dist/context-engine/summarize.js +32 -0
- package/dist/context-engine/telemetry.d.ts +25 -0
- package/dist/context-engine/telemetry.js +64 -0
- package/dist/context-engine/turn-digest.d.ts +50 -0
- package/dist/context-engine/turn-digest.js +250 -0
- package/dist/context-engine/turn-ledger.d.ts +18 -0
- package/dist/context-engine/turn-ledger.js +184 -0
- package/dist/context-engine/turn-recorder.d.ts +24 -0
- package/dist/context-engine/turn-recorder.js +88 -0
- package/dist/context-engine/types.d.ts +201 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-pressure.d.ts +19 -0
- package/dist/context-pressure.js +36 -0
- package/dist/distillers/generic.d.ts +14 -0
- package/dist/distillers/generic.js +93 -0
- package/dist/distillers/git-diff.d.ts +8 -0
- package/dist/distillers/git-diff.js +119 -0
- package/dist/distillers/index.d.ts +2 -0
- package/dist/distillers/index.js +16 -0
- package/dist/distillers/npm-test.d.ts +8 -0
- package/dist/distillers/npm-test.js +50 -0
- package/dist/distillers/rg-results.d.ts +8 -0
- package/dist/distillers/rg-results.js +28 -0
- package/dist/distillers/tsc-errors.d.ts +8 -0
- package/dist/distillers/tsc-errors.js +52 -0
- package/dist/event-log.d.ts +56 -0
- package/dist/event-log.js +214 -0
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +155 -0
- package/dist/logger.d.ts +94 -0
- package/dist/logger.js +287 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +54 -0
- package/dist/memory/confidence.d.ts +7 -0
- package/dist/memory/confidence.js +37 -0
- package/dist/memory/consolidation.d.ts +26 -0
- package/dist/memory/consolidation.js +166 -0
- package/dist/memory/db.d.ts +40 -0
- package/dist/memory/db.js +283 -0
- package/dist/memory/dedup.d.ts +6 -0
- package/dist/memory/dedup.js +50 -0
- package/dist/memory/embedder-factory.d.ts +3 -0
- package/dist/memory/embedder-factory.js +81 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.js +67 -0
- package/dist/memory/index.d.ts +9 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/ollama-summarizer.d.ts +19 -0
- package/dist/memory/ollama-summarizer.js +72 -0
- package/dist/memory/openai-summarizer.d.ts +21 -0
- package/dist/memory/openai-summarizer.js +51 -0
- package/dist/memory/store.d.ts +61 -0
- package/dist/memory/store.js +502 -0
- package/dist/memory/summarizer-factory.d.ts +3 -0
- package/dist/memory/summarizer-factory.js +69 -0
- package/dist/memory/summarizer.d.ts +4 -0
- package/dist/memory/summarizer.js +112 -0
- package/dist/memory/types.d.ts +87 -0
- package/dist/memory/types.js +17 -0
- package/dist/model-context.d.ts +15 -0
- package/dist/model-context.js +212 -0
- package/dist/project-detector.d.ts +37 -0
- package/dist/project-detector.js +604 -0
- package/dist/render.d.ts +15 -0
- package/dist/render.js +46 -0
- package/dist/session.d.ts +118 -0
- package/dist/session.js +809 -0
- package/dist/skills/index.d.ts +69 -0
- package/dist/skills/index.js +885 -0
- package/dist/skills/types.d.ts +93 -0
- package/dist/skills/types.js +8 -0
- package/dist/slash-commands.d.ts +14 -0
- package/dist/slash-commands.js +301 -0
- package/dist/state-graph.d.ts +38 -0
- package/dist/state-graph.js +255 -0
- package/dist/status-bar.d.ts +54 -0
- package/dist/status-bar.js +184 -0
- package/dist/thinking-display.d.ts +21 -0
- package/dist/thinking-display.js +37 -0
- package/dist/tool-summary.d.ts +4 -0
- package/dist/tool-summary.js +67 -0
- package/dist/tools/index.d.ts +925 -0
- package/dist/tools/index.js +86 -0
- package/dist/tools/knowledge.d.ts +140 -0
- package/dist/tools/knowledge.js +260 -0
- package/dist/tools/memory.d.ts +39 -0
- package/dist/tools/memory.js +300 -0
- package/dist/tools/search-code.d.ts +134 -0
- package/dist/tools/search-code.js +390 -0
- package/dist/tools/system.d.ts +16 -0
- package/dist/tools/system.js +499 -0
- package/dist/tools/tool-def.d.ts +6 -0
- package/dist/tools/tool-def.js +3 -0
- package/dist/turn-control.d.ts +51 -0
- package/dist/turn-control.js +210 -0
- package/dist/turn.d.ts +20 -0
- package/dist/turn.js +624 -0
- package/dist/types.d.ts +233 -0
- package/dist/types.js +4 -0
- package/dist/ui/readline-ui.d.ts +2 -0
- package/dist/ui/readline-ui.js +176 -0
- package/dist/ui/tui/app.d.ts +13 -0
- package/dist/ui/tui/app.js +270 -0
- package/dist/ui/tui/busy-indicator.d.ts +2 -0
- package/dist/ui/tui/busy-indicator.js +13 -0
- package/dist/ui/tui/components/gutter-rule.d.ts +5 -0
- package/dist/ui/tui/components/gutter-rule.js +9 -0
- package/dist/ui/tui/components/inline-tool-row.d.ts +10 -0
- package/dist/ui/tui/components/inline-tool-row.js +8 -0
- package/dist/ui/tui/components/prompt-input.d.ts +20 -0
- package/dist/ui/tui/components/prompt-input.js +120 -0
- package/dist/ui/tui/components/system-line.d.ts +5 -0
- package/dist/ui/tui/components/system-line.js +6 -0
- package/dist/ui/tui/components/thinking-block.d.ts +11 -0
- package/dist/ui/tui/components/thinking-block.js +31 -0
- package/dist/ui/tui/components/toast-line.d.ts +4 -0
- package/dist/ui/tui/components/toast-line.js +8 -0
- package/dist/ui/tui/components/tool-result-line.d.ts +5 -0
- package/dist/ui/tui/components/tool-result-line.js +6 -0
- package/dist/ui/tui/components/turn-footer.d.ts +5 -0
- package/dist/ui/tui/components/turn-footer.js +7 -0
- package/dist/ui/tui/components/user-block.d.ts +6 -0
- package/dist/ui/tui/components/user-block.js +6 -0
- package/dist/ui/tui/logo-banner.d.ts +5 -0
- package/dist/ui/tui/logo-banner.js +8 -0
- package/dist/ui/tui/markdown-render.d.ts +16 -0
- package/dist/ui/tui/markdown-render.js +218 -0
- package/dist/ui/tui/palette.d.ts +12 -0
- package/dist/ui/tui/palette.js +13 -0
- package/dist/ui/tui/reasoning-summary.d.ts +12 -0
- package/dist/ui/tui/reasoning-summary.js +27 -0
- package/dist/ui/tui/reducer.d.ts +92 -0
- package/dist/ui/tui/reducer.js +260 -0
- package/dist/ui/tui/run.d.ts +3 -0
- package/dist/ui/tui/run.js +40 -0
- package/dist/ui/tui/sink.d.ts +4 -0
- package/dist/ui/tui/sink.js +89 -0
- package/dist/ui/tui/status-bar-view.d.ts +5 -0
- package/dist/ui/tui/status-bar-view.js +44 -0
- package/dist/ui/tui/terminal-height.d.ts +12 -0
- package/dist/ui/tui/terminal-height.js +20 -0
- package/dist/ui/tui/terminal-width.d.ts +2 -0
- package/dist/ui/tui/terminal-width.js +5 -0
- package/dist/ui/tui/tool-display.d.ts +23 -0
- package/dist/ui/tui/tool-display.js +217 -0
- package/dist/ui/tui/transcript-line.d.ts +12 -0
- package/dist/ui/tui/transcript-line.js +43 -0
- package/dist/ui/tui/transcript-replay.d.ts +12 -0
- package/dist/ui/tui/transcript-replay.js +117 -0
- package/dist/ui-events.d.ts +39 -0
- package/dist/ui-events.js +33 -0
- package/dist/ui.d.ts +77 -0
- package/dist/ui.js +179 -0
- package/package.json +73 -0
- package/praana.config.example.toml +231 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ARIA Memory — OpenAI/OpenRouter Summarizer Adapter
|
|
3
|
+
// ============================================================
|
|
4
|
+
export class OpenAISummarizer {
|
|
5
|
+
name;
|
|
6
|
+
baseUrl;
|
|
7
|
+
apiKey;
|
|
8
|
+
model;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
11
|
+
this.apiKey = opts.apiKey;
|
|
12
|
+
this.model = opts.model;
|
|
13
|
+
this.name = `openai:${opts.model}`;
|
|
14
|
+
}
|
|
15
|
+
async available() {
|
|
16
|
+
return this.apiKey.length > 0;
|
|
17
|
+
}
|
|
18
|
+
async complete(opts) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 60_000);
|
|
21
|
+
try {
|
|
22
|
+
const messages = [];
|
|
23
|
+
if (opts.system)
|
|
24
|
+
messages.push({ role: "system", content: opts.system });
|
|
25
|
+
messages.push({ role: "user", content: opts.prompt });
|
|
26
|
+
const body = { model: this.model, messages };
|
|
27
|
+
if (opts.temperature !== undefined)
|
|
28
|
+
body.temperature = opts.temperature;
|
|
29
|
+
if (opts.maxTokens !== undefined)
|
|
30
|
+
body.max_tokens = opts.maxTokens;
|
|
31
|
+
if (opts.json)
|
|
32
|
+
body.response_format = { type: "json_object" };
|
|
33
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
throw new Error(`Summarizer ${res.status}: ${await res.text()}`);
|
|
44
|
+
const json = (await res.json());
|
|
45
|
+
return json.choices[0]?.message?.content ?? "";
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Embedder } from "./types.js";
|
|
2
|
+
import type { Digest, MemoryEntry, RecallOptions, RecallResult, RememberOptions, SessionContext, SessionEvent, SummarizerLLM } from "./types.js";
|
|
3
|
+
export declare class MemoryStore {
|
|
4
|
+
private db;
|
|
5
|
+
private embedder;
|
|
6
|
+
private summarizer;
|
|
7
|
+
private defaultScopes;
|
|
8
|
+
private sessionId;
|
|
9
|
+
/** True while a background re-embed migration is running. */
|
|
10
|
+
private reembedding;
|
|
11
|
+
private reembedPromise;
|
|
12
|
+
constructor(opts: {
|
|
13
|
+
dbPath: string;
|
|
14
|
+
embedder: Embedder;
|
|
15
|
+
summarizer?: SummarizerLLM | null;
|
|
16
|
+
/**
|
|
17
|
+
* Override the auto-detected needsReembed flag from openMemoryDb.
|
|
18
|
+
* Intended for tests that use :memory: DBs with explicit control.
|
|
19
|
+
*/
|
|
20
|
+
needsReembed?: boolean;
|
|
21
|
+
});
|
|
22
|
+
close(): void;
|
|
23
|
+
/** Get the summarizer LLM (may be null if summarizer is disabled). */
|
|
24
|
+
getSummarizer(): SummarizerLLM | null;
|
|
25
|
+
getEntryCount(): number;
|
|
26
|
+
sessionStart(ctx: SessionContext): Promise<Digest>;
|
|
27
|
+
sessionEnd(reason: string, events?: SessionEvent[]): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Compress a batch of old turns into episodic facts.
|
|
30
|
+
* Returns the number of facts stored.
|
|
31
|
+
*/
|
|
32
|
+
compressTurns(events: SessionEvent[]): Promise<number>;
|
|
33
|
+
remember(content: string, opts?: RememberOptions): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
}>;
|
|
36
|
+
private storeLearning;
|
|
37
|
+
recall(query: string, opts?: RecallOptions): Promise<RecallResult>;
|
|
38
|
+
pin(id: string): Promise<void>;
|
|
39
|
+
unpin(id: string): Promise<void>;
|
|
40
|
+
reinforceFromSuccessfulToolOutcome(entryIds: string[], alpha?: number): void;
|
|
41
|
+
getAllEntries(): MemoryEntry[];
|
|
42
|
+
/** Promote an entry from Layer 1 to Layer 2 (deep memory). */
|
|
43
|
+
promoteToLayer2(id: string): void;
|
|
44
|
+
/** Weaken an entry's confidence by a beta factor (0–1). */
|
|
45
|
+
weakenEntry(id: string, beta?: number): void;
|
|
46
|
+
/**
|
|
47
|
+
* Remove stale Layer 1 entries with effective confidence below 0.05
|
|
48
|
+
* that have not been seen in 30+ days. Never prunes pinned or Layer 2 entries.
|
|
49
|
+
*/
|
|
50
|
+
prune(): Promise<number>;
|
|
51
|
+
/** Re-embed all entries after vector dimension migration. Runs in the background. */
|
|
52
|
+
reembedAllEntries(): Promise<void>;
|
|
53
|
+
private waitForReembed;
|
|
54
|
+
private buildDefaultScopes;
|
|
55
|
+
private buildDigest;
|
|
56
|
+
private buildScopeQueries;
|
|
57
|
+
private getEntriesForScopeQueries;
|
|
58
|
+
private entryMatchesScopeQuery;
|
|
59
|
+
retractMemory(id: string): void;
|
|
60
|
+
hasEntry(id: string): boolean;
|
|
61
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ARIA Memory — Store
|
|
3
|
+
//
|
|
4
|
+
// Unified API: remember, recall, digest, pin, session lifecycle.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { ulid } from "ulid";
|
|
7
|
+
import { clearReembedNeeded, endSessionRow, flushReinforcements, getAllEntries, getEntriesByScope, getEntryById, insertEntry, deleteEntry, retractMemory as retractMemoryDb, openMemoryDb, reinforceEntry, searchByFts, searchByVector, stampReinforcement, startSessionRow, touchEntry, upsertEmbedding, weakenEntry, } from "./db.js";
|
|
8
|
+
import { extractLearnings, summarizeTurns } from "./summarizer.js";
|
|
9
|
+
import { CONTRADICTION_MATCH_THRESHOLD, isContradiction, isNearDuplicate, } from "./dedup.js";
|
|
10
|
+
import { isMemoryKind, MEMORY_KINDS } from "./types.js";
|
|
11
|
+
import { effectiveConfidence, digestScore } from "./confidence.js";
|
|
12
|
+
import { getAppLogger } from "../logger.js";
|
|
13
|
+
import { APP_AGENT_ID, LEGACY_APP_AGENT_ID } from "../app-identity.js";
|
|
14
|
+
function certaintyToConfidence(c) {
|
|
15
|
+
return c === "high" ? 0.8 : c === "medium" ? 0.5 : 0.3;
|
|
16
|
+
}
|
|
17
|
+
function queryTerms(query) {
|
|
18
|
+
return query.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
|
|
19
|
+
}
|
|
20
|
+
function isAbortLikeError(err) {
|
|
21
|
+
if (!(err instanceof Error))
|
|
22
|
+
return false;
|
|
23
|
+
if (err.name === "AbortError")
|
|
24
|
+
return true;
|
|
25
|
+
return /\babort(ed)?\b/i.test(err.message);
|
|
26
|
+
}
|
|
27
|
+
function lexicalMatchScore(entry, terms, rankScore) {
|
|
28
|
+
if (terms.length === 0)
|
|
29
|
+
return 0;
|
|
30
|
+
const content = entry.content.toLowerCase();
|
|
31
|
+
const matched = terms.filter((term) => content.includes(term)).length;
|
|
32
|
+
const coverage = matched / terms.length;
|
|
33
|
+
return 0.6 + coverage * 0.3 + rankScore * 0.1;
|
|
34
|
+
}
|
|
35
|
+
export class MemoryStore {
|
|
36
|
+
db;
|
|
37
|
+
embedder;
|
|
38
|
+
summarizer;
|
|
39
|
+
defaultScopes = [];
|
|
40
|
+
sessionId = "";
|
|
41
|
+
/** True while a background re-embed migration is running. */
|
|
42
|
+
reembedding = false;
|
|
43
|
+
reembedPromise = null;
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
const opened = openMemoryDb(opts.dbPath, opts.embedder.dim);
|
|
46
|
+
this.db = opened.db;
|
|
47
|
+
this.embedder = opts.embedder;
|
|
48
|
+
this.summarizer = opts.summarizer ?? null;
|
|
49
|
+
if (opts.needsReembed ?? opened.needsReembed) {
|
|
50
|
+
this.reembedPromise = this.reembedAllEntries();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
close() {
|
|
54
|
+
this.db.close();
|
|
55
|
+
}
|
|
56
|
+
/** Get the summarizer LLM (may be null if summarizer is disabled). */
|
|
57
|
+
getSummarizer() {
|
|
58
|
+
return this.summarizer;
|
|
59
|
+
}
|
|
60
|
+
getEntryCount() {
|
|
61
|
+
return getAllEntries(this.db).length;
|
|
62
|
+
}
|
|
63
|
+
// ---- Session lifecycle ----
|
|
64
|
+
async sessionStart(ctx) {
|
|
65
|
+
await this.waitForReembed();
|
|
66
|
+
this.sessionId = ulid();
|
|
67
|
+
this.defaultScopes = this.buildDefaultScopes(ctx);
|
|
68
|
+
const pruned = await this.prune();
|
|
69
|
+
if (pruned > 0) {
|
|
70
|
+
getAppLogger().child("memory").info(`Pruned ${pruned} stale Layer 1 ${pruned === 1 ? "entry" : "entries"}`);
|
|
71
|
+
}
|
|
72
|
+
startSessionRow(this.db, {
|
|
73
|
+
id: this.sessionId,
|
|
74
|
+
agent: ctx.agent,
|
|
75
|
+
user_id: ctx.user_id,
|
|
76
|
+
context_id: ctx.context_id,
|
|
77
|
+
started_at: ctx.time,
|
|
78
|
+
});
|
|
79
|
+
return this.buildDigest(ctx, ctx.recall_min_score);
|
|
80
|
+
}
|
|
81
|
+
async sessionEnd(reason, events) {
|
|
82
|
+
flushReinforcements(this.db, this.sessionId);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
endSessionRow(this.db, this.sessionId, now, reason);
|
|
85
|
+
if (events && events.length > 0 && this.summarizer) {
|
|
86
|
+
try {
|
|
87
|
+
const learnings = await extractLearnings(this.summarizer, events);
|
|
88
|
+
for (const l of learnings) {
|
|
89
|
+
await this.storeLearning(l);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (isAbortLikeError(err)) {
|
|
94
|
+
getAppLogger().child("memory").warn("Session-end summarizer aborted; skipping learnings for this session");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compress a batch of old turns into episodic facts.
|
|
103
|
+
* Returns the number of facts stored.
|
|
104
|
+
*/
|
|
105
|
+
async compressTurns(events) {
|
|
106
|
+
if (!this.summarizer)
|
|
107
|
+
return 0;
|
|
108
|
+
try {
|
|
109
|
+
const facts = await summarizeTurns(this.summarizer, events);
|
|
110
|
+
for (const fact of facts) {
|
|
111
|
+
await this.remember(fact.content, {
|
|
112
|
+
kind: fact.kind,
|
|
113
|
+
certainty: fact.certainty,
|
|
114
|
+
pinned: false,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return facts.length;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (isAbortLikeError(err)) {
|
|
121
|
+
getAppLogger().child("memory").warn("Turn compression aborted; skipping");
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ---- Core operations ----
|
|
128
|
+
async remember(content, opts = {}) {
|
|
129
|
+
const id = ulid();
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const kind = opts.kind ?? "fact";
|
|
132
|
+
if (!isMemoryKind(kind)) {
|
|
133
|
+
throw new Error(`Invalid memory kind: '${kind}'. Valid kinds: ${MEMORY_KINDS.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
const confidence = certaintyToConfidence(opts.certainty ?? "medium");
|
|
136
|
+
const scopes = opts.scope ?? this.defaultScopes;
|
|
137
|
+
const entry = {
|
|
138
|
+
id,
|
|
139
|
+
kind,
|
|
140
|
+
content: content.slice(0, 1000),
|
|
141
|
+
confidence,
|
|
142
|
+
pinned: opts.pinned ?? false,
|
|
143
|
+
layer: 1,
|
|
144
|
+
confirmation_count: 0,
|
|
145
|
+
created_at: now,
|
|
146
|
+
last_seen_at: now,
|
|
147
|
+
session_id: this.sessionId,
|
|
148
|
+
scopes,
|
|
149
|
+
retracted: false,
|
|
150
|
+
};
|
|
151
|
+
insertEntry(this.db, entry);
|
|
152
|
+
// Embedding (fire-and-forget)
|
|
153
|
+
this.embedder.embed(content).then((vec) => {
|
|
154
|
+
upsertEmbedding(this.db, id, vec);
|
|
155
|
+
}).catch(() => { });
|
|
156
|
+
return { id };
|
|
157
|
+
}
|
|
158
|
+
async storeLearning(learning) {
|
|
159
|
+
const similar = await this.recall(learning.content, {
|
|
160
|
+
limit: 3,
|
|
161
|
+
kinds: [learning.kind],
|
|
162
|
+
});
|
|
163
|
+
const duplicate = similar.entries.find((e) => {
|
|
164
|
+
const existing = getEntryById(this.db, e.id);
|
|
165
|
+
if (!existing)
|
|
166
|
+
return false;
|
|
167
|
+
return isNearDuplicate(existing.content, learning.content, Math.max(e.match, e.score));
|
|
168
|
+
});
|
|
169
|
+
if (duplicate) {
|
|
170
|
+
reinforceEntry(this.db, duplicate.id, 0.08);
|
|
171
|
+
this.db
|
|
172
|
+
.prepare("UPDATE entries SET confirmation_count = confirmation_count + 1 WHERE id = ?")
|
|
173
|
+
.run(duplicate.id);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const candidate of similar.entries) {
|
|
177
|
+
if (candidate.match < CONTRADICTION_MATCH_THRESHOLD && candidate.score < CONTRADICTION_MATCH_THRESHOLD) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const existing = getEntryById(this.db, candidate.id);
|
|
181
|
+
if (!existing)
|
|
182
|
+
continue;
|
|
183
|
+
if (await isContradiction(existing.content, learning.content, this.summarizer)) {
|
|
184
|
+
weakenEntry(this.db, candidate.id, 0.15);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await this.remember(learning.content, {
|
|
188
|
+
kind: learning.kind,
|
|
189
|
+
certainty: learning.certainty,
|
|
190
|
+
scope: learning.scope_hints ?? this.defaultScopes,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async recall(query, opts = {}) {
|
|
194
|
+
const limit = opts.limit ?? 10;
|
|
195
|
+
const queryScopes = opts.scope ?? this.defaultScopes;
|
|
196
|
+
const scopeQueries = this.buildScopeQueries(queryScopes);
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
if (this.reembedding) {
|
|
199
|
+
getAppLogger().child("memory").warn("Vector migration in progress — recall quality may be reduced until re-embed completes");
|
|
200
|
+
}
|
|
201
|
+
// Strategy: merge reliable keyword hits with vector candidates, then filter + re-rank.
|
|
202
|
+
const candidateScores = new Map();
|
|
203
|
+
const terms = queryTerms(query);
|
|
204
|
+
const addCandidate = (entry, matchScore) => {
|
|
205
|
+
const existing = candidateScores.get(entry.id);
|
|
206
|
+
if (!existing || matchScore > existing.matchScore) {
|
|
207
|
+
candidateScores.set(entry.id, { entry, matchScore });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const matchesAnyScopeQuery = (entry) => {
|
|
211
|
+
if (scopeQueries.length === 0)
|
|
212
|
+
return true;
|
|
213
|
+
return scopeQueries.some((scopes) => this.entryMatchesScopeQuery(entry, scopes));
|
|
214
|
+
};
|
|
215
|
+
const matchesRequestedFilters = (entry) => {
|
|
216
|
+
if (!matchesAnyScopeQuery(entry))
|
|
217
|
+
return false;
|
|
218
|
+
if (opts.kinds && opts.kinds.length > 0 && !opts.kinds.includes(entry.kind)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
};
|
|
223
|
+
const ftsHits = scopeQueries.flatMap((scopes) => searchByFts(this.db, query, limit * 4, {
|
|
224
|
+
scopes,
|
|
225
|
+
kinds: opts.kinds,
|
|
226
|
+
}));
|
|
227
|
+
const ftsRanks = ftsHits.map((h) => h.rank);
|
|
228
|
+
const bestFtsRank = Math.min(...ftsRanks);
|
|
229
|
+
const worstFtsRank = Math.max(...ftsRanks);
|
|
230
|
+
for (const h of ftsHits) {
|
|
231
|
+
const e = getEntryById(this.db, h.entry_id);
|
|
232
|
+
if (e) {
|
|
233
|
+
const rankScore = bestFtsRank === worstFtsRank
|
|
234
|
+
? 1
|
|
235
|
+
: 1 - ((h.rank - bestFtsRank) / (worstFtsRank - bestFtsRank));
|
|
236
|
+
addCandidate(e, lexicalMatchScore(e, terms, rankScore));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const qvec = await this.embedder.embed(query);
|
|
241
|
+
const hits = searchByVector(this.db, qvec, limit * 4);
|
|
242
|
+
for (const h of hits) {
|
|
243
|
+
const e = getEntryById(this.db, h.entry_id);
|
|
244
|
+
if (e) {
|
|
245
|
+
const similarity = Math.max(0, 1 - h.distance);
|
|
246
|
+
addCandidate(e, Math.min(similarity, 0.75));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Vector search failed — fall back to scope-only
|
|
252
|
+
}
|
|
253
|
+
// If neither search path returned candidates, fall back to all entries in scope.
|
|
254
|
+
if (candidateScores.size === 0) {
|
|
255
|
+
for (const e of this.getEntriesForScopeQueries(scopeQueries)) {
|
|
256
|
+
addCandidate(e, 0);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
let candidates = Array.from(candidateScores.values());
|
|
260
|
+
// Filter out retracted (tombstoned) entries
|
|
261
|
+
candidates = candidates.filter(({ entry }) => !entry.retracted);
|
|
262
|
+
// Enforce strict scope isolation: entry must include ALL requested scopes.
|
|
263
|
+
// This keeps vector and fallback paths consistent.
|
|
264
|
+
candidates = candidates.filter(({ entry }) => matchesAnyScopeQuery(entry));
|
|
265
|
+
// Filter by kind
|
|
266
|
+
if (opts.kinds && opts.kinds.length > 0) {
|
|
267
|
+
const kindSet = new Set(opts.kinds);
|
|
268
|
+
candidates = candidates.filter(({ entry }) => kindSet.has(entry.kind));
|
|
269
|
+
}
|
|
270
|
+
// Vector search is limited before scope filtering by sqlite-vec. If its top
|
|
271
|
+
// hits are outside the requested scopes, preserve the old scoped fallback.
|
|
272
|
+
if (candidates.length === 0 && candidateScores.size > 0) {
|
|
273
|
+
for (const e of this.getEntriesForScopeQueries(scopeQueries)) {
|
|
274
|
+
if (!opts.kinds || opts.kinds.includes(e.kind)) {
|
|
275
|
+
addCandidate(e, 0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
candidates = Array.from(candidateScores.values()).filter(({ entry }) => {
|
|
279
|
+
return matchesRequestedFilters(entry);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// Score & rank
|
|
283
|
+
const scored = candidates.map(({ entry: e, matchScore }) => {
|
|
284
|
+
const conf = effectiveConfidence(e, now);
|
|
285
|
+
const match = matchScore;
|
|
286
|
+
// Recency bonus: 0–0.2 based on days since last seen (max at 0 days)
|
|
287
|
+
const daysSince = (now - e.last_seen_at) / (1000 * 60 * 60 * 24);
|
|
288
|
+
const recency = Math.max(0, 0.2 - daysSince * 0.02);
|
|
289
|
+
// Pin bonus
|
|
290
|
+
const pin = e.pinned ? 0.3 : 0;
|
|
291
|
+
const score = matchScore + conf * 0.2 + recency + pin;
|
|
292
|
+
return { entry: e, score, match, conf };
|
|
293
|
+
});
|
|
294
|
+
scored.sort((a, b) => {
|
|
295
|
+
if (b.score !== a.score)
|
|
296
|
+
return b.score - a.score;
|
|
297
|
+
return b.conf - a.conf;
|
|
298
|
+
});
|
|
299
|
+
// Touch recalled entries
|
|
300
|
+
const top = scored.slice(0, limit);
|
|
301
|
+
for (const s of top) {
|
|
302
|
+
touchEntry(this.db, s.entry.id, now);
|
|
303
|
+
stampReinforcement(this.db, s.entry.id, this.sessionId);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
entries: top.map((s) => ({
|
|
307
|
+
id: s.entry.id,
|
|
308
|
+
kind: s.entry.kind,
|
|
309
|
+
content: s.entry.content,
|
|
310
|
+
confidence: s.entry.confidence,
|
|
311
|
+
match: Math.round(s.match * 1000) / 1000,
|
|
312
|
+
scopes: s.entry.scopes,
|
|
313
|
+
score: Math.round(s.score * 1000) / 1000,
|
|
314
|
+
})),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async pin(id) {
|
|
318
|
+
this.db.prepare("UPDATE entries SET pinned = 1 WHERE id = ?").run(id);
|
|
319
|
+
}
|
|
320
|
+
async unpin(id) {
|
|
321
|
+
this.db.prepare("UPDATE entries SET pinned = 0 WHERE id = ?").run(id);
|
|
322
|
+
}
|
|
323
|
+
reinforceFromSuccessfulToolOutcome(entryIds, alpha = 0.08) {
|
|
324
|
+
const uniqueIds = new Set(entryIds);
|
|
325
|
+
for (const id of uniqueIds) {
|
|
326
|
+
const entry = getEntryById(this.db, id);
|
|
327
|
+
if (!entry)
|
|
328
|
+
continue;
|
|
329
|
+
reinforceEntry(this.db, id, alpha);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
getAllEntries() {
|
|
333
|
+
return getAllEntries(this.db);
|
|
334
|
+
}
|
|
335
|
+
/** Promote an entry from Layer 1 to Layer 2 (deep memory). */
|
|
336
|
+
promoteToLayer2(id) {
|
|
337
|
+
this.db.prepare("UPDATE entries SET layer = 2 WHERE id = ? AND layer = 1").run(id);
|
|
338
|
+
}
|
|
339
|
+
/** Weaken an entry's confidence by a beta factor (0–1). */
|
|
340
|
+
weakenEntry(id, beta = 0.3) {
|
|
341
|
+
weakenEntry(this.db, id, beta);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Remove stale Layer 1 entries with effective confidence below 0.05
|
|
345
|
+
* that have not been seen in 30+ days. Never prunes pinned or Layer 2 entries.
|
|
346
|
+
*/
|
|
347
|
+
async prune() {
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const minAgeMs = 30 * 86_400_000;
|
|
350
|
+
const toDelete = getAllEntries(this.db).filter((e) => {
|
|
351
|
+
if (e.pinned || e.layer === 2)
|
|
352
|
+
return false;
|
|
353
|
+
const ageMs = now - e.last_seen_at;
|
|
354
|
+
if (ageMs <= minAgeMs)
|
|
355
|
+
return false;
|
|
356
|
+
return effectiveConfidence(e, now) < 0.05;
|
|
357
|
+
});
|
|
358
|
+
for (const entry of toDelete) {
|
|
359
|
+
deleteEntry(this.db, entry.id);
|
|
360
|
+
}
|
|
361
|
+
return toDelete.length;
|
|
362
|
+
}
|
|
363
|
+
/** Re-embed all entries after vector dimension migration. Runs in the background. */
|
|
364
|
+
async reembedAllEntries() {
|
|
365
|
+
this.reembedding = true;
|
|
366
|
+
const entries = getAllEntries(this.db);
|
|
367
|
+
const log = getAppLogger().child("memory");
|
|
368
|
+
log.info(`Re-embedding ${entries.length} entries after dimension migration…`);
|
|
369
|
+
let failed = 0;
|
|
370
|
+
for (const e of entries) {
|
|
371
|
+
try {
|
|
372
|
+
const vec = await this.embedder.embed(e.content);
|
|
373
|
+
upsertEmbedding(this.db, e.id, vec);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
failed++;
|
|
377
|
+
log.warn(`Failed to re-embed entry ${e.id}`, {
|
|
378
|
+
cause: err instanceof Error ? err : new Error(String(err)),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this.reembedding = false;
|
|
383
|
+
if (failed > 0) {
|
|
384
|
+
log.warn(`Re-embed migration complete — ${failed}/${entries.length} entries failed. Recall quality may be degraded`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
clearReembedNeeded(this.db);
|
|
388
|
+
log.info(`Re-embed migration complete — ${entries.length} entries updated`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// ---- Internal ----
|
|
392
|
+
async waitForReembed() {
|
|
393
|
+
if (this.reembedPromise) {
|
|
394
|
+
await this.reembedPromise;
|
|
395
|
+
this.reembedPromise = null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
buildDefaultScopes(ctx) {
|
|
399
|
+
return [
|
|
400
|
+
`user:${ctx.user_id}`,
|
|
401
|
+
`agent:${ctx.agent}`,
|
|
402
|
+
`context:${ctx.context_id}`,
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
async buildDigest(ctx, minScore = 0.35) {
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const entries = this.getEntriesForScopeQueries(this.buildScopeQueries(this.defaultScopes));
|
|
408
|
+
// Score all entries — Layer 2 first, then digestScore within layer
|
|
409
|
+
const scored = entries.map((e) => ({
|
|
410
|
+
entry: e,
|
|
411
|
+
score: digestScore(e, now) + (e.pinned ? 0.3 : 0),
|
|
412
|
+
}));
|
|
413
|
+
scored.sort((a, b) => {
|
|
414
|
+
if (a.entry.layer !== b.entry.layer)
|
|
415
|
+
return b.entry.layer - a.entry.layer;
|
|
416
|
+
return b.score - a.score;
|
|
417
|
+
});
|
|
418
|
+
const filtered = scored.filter((s) => s.score >= minScore);
|
|
419
|
+
// Build markdown
|
|
420
|
+
const lines = [];
|
|
421
|
+
const included = [];
|
|
422
|
+
const kindOrder = ["constraint", "preference", "fact", "pattern", "decision", "mistake"];
|
|
423
|
+
for (const kind of kindOrder) {
|
|
424
|
+
const bucket = filtered
|
|
425
|
+
.filter((s) => s.entry.kind === kind)
|
|
426
|
+
.sort((a, b) => b.score - a.score);
|
|
427
|
+
if (bucket.length === 0)
|
|
428
|
+
continue;
|
|
429
|
+
const title = kind.charAt(0).toUpperCase() + kind.slice(1) + (kind === "mistake" ? "s to avoid" : "s");
|
|
430
|
+
lines.push(`## ${title}`);
|
|
431
|
+
for (const s of bucket) {
|
|
432
|
+
lines.push(`- ${s.entry.content}`);
|
|
433
|
+
included.push(s.entry.id);
|
|
434
|
+
}
|
|
435
|
+
lines.push("");
|
|
436
|
+
}
|
|
437
|
+
if (lines.length > 0) {
|
|
438
|
+
lines.push("Use recall(\"...\") for anything not shown.");
|
|
439
|
+
}
|
|
440
|
+
const markdown = lines.join("\n").trimEnd();
|
|
441
|
+
return {
|
|
442
|
+
markdown: markdown || "_No memories for this scope yet._",
|
|
443
|
+
empty: included.length === 0,
|
|
444
|
+
entriesIncluded: included,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
buildScopeQueries(scopes) {
|
|
448
|
+
if (scopes.length === 0)
|
|
449
|
+
return [scopes];
|
|
450
|
+
const queries = [scopes];
|
|
451
|
+
const legacyAgentScopes = scopes.map((scope) => scope === `agent:${APP_AGENT_ID}` ? `agent:${LEGACY_APP_AGENT_ID}` : scope);
|
|
452
|
+
if (legacyAgentScopes.some((scope, index) => scope !== scopes[index])) {
|
|
453
|
+
queries.push(legacyAgentScopes);
|
|
454
|
+
}
|
|
455
|
+
const hasContextScope = scopes.some((scope) => scope.startsWith("context:"));
|
|
456
|
+
if (!hasContextScope)
|
|
457
|
+
return queries;
|
|
458
|
+
const globalScopes = scopes.filter((scope) => !scope.startsWith("context:"));
|
|
459
|
+
if (globalScopes.length === scopes.length || globalScopes.length === 0) {
|
|
460
|
+
return queries;
|
|
461
|
+
}
|
|
462
|
+
queries.push(globalScopes);
|
|
463
|
+
return queries;
|
|
464
|
+
}
|
|
465
|
+
getEntriesForScopeQueries(scopeQueries) {
|
|
466
|
+
const byId = new Map();
|
|
467
|
+
for (const scopes of scopeQueries) {
|
|
468
|
+
const entries = scopes.length > 0
|
|
469
|
+
? getEntriesByScope(this.db, scopes)
|
|
470
|
+
: getAllEntries(this.db);
|
|
471
|
+
for (const entry of entries) {
|
|
472
|
+
if (entry.retracted)
|
|
473
|
+
continue;
|
|
474
|
+
if (!this.entryMatchesScopeQuery(entry, scopes))
|
|
475
|
+
continue;
|
|
476
|
+
if (!byId.has(entry.id))
|
|
477
|
+
byId.set(entry.id, entry);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return Array.from(byId.values());
|
|
481
|
+
}
|
|
482
|
+
entryMatchesScopeQuery(entry, scopes) {
|
|
483
|
+
const entryScopeSet = new Set(entry.scopes);
|
|
484
|
+
for (const scope of scopes) {
|
|
485
|
+
if (!entryScopeSet.has(scope))
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
const queryHasContext = scopes.some((scope) => scope.startsWith("context:"));
|
|
489
|
+
if (!queryHasContext) {
|
|
490
|
+
const entryHasContext = entry.scopes.some((scope) => scope.startsWith("context:"));
|
|
491
|
+
if (entryHasContext)
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
retractMemory(id) {
|
|
497
|
+
retractMemoryDb(this.db, id);
|
|
498
|
+
}
|
|
499
|
+
hasEntry(id) {
|
|
500
|
+
return getEntryById(this.db, id) !== undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ARIA Memory — Summarizer factory
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { getAppLogger } from "../logger.js";
|
|
5
|
+
import { envOverride } from "../app-identity.js";
|
|
6
|
+
import { OllamaEmbedder } from "./embeddings.js";
|
|
7
|
+
import { OllamaSummarizer, listOllamaModelNames, pickDefaultChatModel, } from "./ollama-summarizer.js";
|
|
8
|
+
import { OpenAISummarizer } from "./openai-summarizer.js";
|
|
9
|
+
export async function createSummarizer(config) {
|
|
10
|
+
const log = getAppLogger().child("memory");
|
|
11
|
+
const mode = (config.summarizer ?? "openrouter").toLowerCase();
|
|
12
|
+
if (mode === "disabled")
|
|
13
|
+
return null;
|
|
14
|
+
if (mode === "ollama") {
|
|
15
|
+
const url = config.ollama_url ?? "http://localhost:11434";
|
|
16
|
+
const configured = config.ollama_summarizer_model?.trim() ||
|
|
17
|
+
envOverride("PRAANA_SUMMARIZER_MODEL", "ARIA_SUMMARIZER_MODEL")?.trim() ||
|
|
18
|
+
"";
|
|
19
|
+
let model = configured;
|
|
20
|
+
if (!model) {
|
|
21
|
+
const names = await listOllamaModelNames(url);
|
|
22
|
+
model = pickDefaultChatModel(names) ?? "";
|
|
23
|
+
if (model) {
|
|
24
|
+
log.notice(`summarizer model unset — using: ${model}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!model) {
|
|
28
|
+
log.warn("Ollama summarizer enabled but no chat model found. Set memory.ollama_summarizer_model or run: ollama pull <model>");
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!(await OllamaEmbedder.isAvailable(url, model))) {
|
|
32
|
+
log.warn(`Ollama model '${model}' is not available at ${url}. Run: ollama pull ${model.split(":")[0]}`);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
log.notice(`summarizer: ${model}`);
|
|
36
|
+
return new OllamaSummarizer(url, model);
|
|
37
|
+
}
|
|
38
|
+
if (mode === "openai") {
|
|
39
|
+
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
40
|
+
const model = envOverride("PRAANA_SUMMARIZER_MODEL", "ARIA_SUMMARIZER_MODEL") ?? "gpt-4o-mini";
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
log.warn("summarizer=openai but OPENAI_API_KEY is not set");
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return new OpenAISummarizer({
|
|
46
|
+
baseUrl: "https://api.openai.com/v1",
|
|
47
|
+
apiKey,
|
|
48
|
+
model,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// openrouter (default) and legacy "openrouter" spelling
|
|
52
|
+
if (mode === "openrouter" || mode === "openai-compatible") {
|
|
53
|
+
const openRouterKey = process.env.OPENROUTER_API_KEY ?? "";
|
|
54
|
+
const openAiKey = process.env.OPENAI_API_KEY ?? "";
|
|
55
|
+
const apiKey = openRouterKey || openAiKey;
|
|
56
|
+
const baseUrl = openRouterKey
|
|
57
|
+
? "https://openrouter.ai/api/v1"
|
|
58
|
+
: "https://api.openai.com/v1";
|
|
59
|
+
const model = envOverride("PRAANA_SUMMARIZER_MODEL", "ARIA_SUMMARIZER_MODEL") ??
|
|
60
|
+
"google/gemini-2.5-flash";
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
log.warn("summarizer=openrouter but OPENROUTER_API_KEY / OPENAI_API_KEY is not set");
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return new OpenAISummarizer({ baseUrl, apiKey, model });
|
|
66
|
+
}
|
|
67
|
+
log.warn(`Unknown memory.summarizer '${config.summarizer}' — session-end summarization disabled`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|