@deepstrike/wasm 0.2.39 → 0.2.41
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 +14 -13
- package/dist/harness/index.d.ts +127 -61
- package/dist/harness/index.js +212 -83
- package/dist/index.d.ts +13 -6
- package/dist/index.js +7 -2
- package/dist/memory/extraction.d.ts +4 -0
- package/dist/memory/extraction.js +48 -0
- package/dist/memory/in-memory-store.d.ts +19 -9
- package/dist/memory/in-memory-store.js +68 -23
- package/dist/memory/index.d.ts +52 -24
- package/dist/memory/ranking.d.ts +32 -0
- package/dist/memory/ranking.js +77 -0
- package/dist/memory/retention.d.ts +17 -0
- package/dist/memory/retention.js +54 -0
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/base.js +32 -0
- package/dist/runtime/context-policy.d.ts +35 -0
- package/dist/runtime/context-policy.js +66 -0
- package/dist/runtime/eval.d.ts +2 -0
- package/dist/runtime/execution-plane.d.ts +2 -1
- package/dist/runtime/execution-plane.js +3 -3
- package/dist/runtime/facade.js +2 -1
- package/dist/runtime/index.d.ts +13 -4
- package/dist/runtime/index.js +6 -1
- package/dist/runtime/kernel-event-log.js +46 -13
- package/dist/runtime/kernel-rebuild.d.ts +8 -0
- package/dist/runtime/kernel-rebuild.js +65 -0
- package/dist/runtime/kernel-step.d.ts +139 -7
- package/dist/runtime/kernel-step.js +110 -10
- package/dist/runtime/kernel-transaction-log.d.ts +61 -0
- package/dist/runtime/kernel-transaction-log.js +140 -0
- package/dist/runtime/os-profile.d.ts +9 -10
- package/dist/runtime/os-profile.js +14 -10
- package/dist/runtime/os-snapshot.d.ts +19 -0
- package/dist/runtime/os-snapshot.js +16 -3
- package/dist/runtime/runner.d.ts +73 -41
- package/dist/runtime/runner.js +562 -290
- package/dist/runtime/session-log.d.ts +55 -10
- package/dist/runtime/session-log.js +60 -3
- package/dist/runtime/session-repair.d.ts +11 -7
- package/dist/runtime/session-repair.js +11 -8
- package/dist/runtime/sub-agent-orchestrator.js +1 -1
- package/dist/runtime/types/agent.d.ts +31 -0
- package/dist/runtime/types/agent.js +35 -0
- package/dist/signals/index.d.ts +21 -1
- package/dist/signals/index.js +1 -0
- package/dist/types.d.ts +7 -2
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const KINDS = new Set(["user", "feedback", "project", "reference"]);
|
|
2
|
+
export async function extractSessionMemories(provider, session, scope, systemPrompt) {
|
|
3
|
+
const transcript = session.messages.map(message => `[${message.role.toUpperCase()}] ${message.content}`).join("\n").slice(0, 8_000);
|
|
4
|
+
const context = {
|
|
5
|
+
systemText: [systemPrompt, "Extract durable, reusable facts from this completed session. Return only JSON; do not include transient progress or guesses."].filter(Boolean).join("\n\n"),
|
|
6
|
+
turns: [{ role: "user", content: `${transcript}\n\nReturn {"memories":[{"name":"stable-kebab-key","kind":"user|feedback|project|reference","content":"fact","description":"why durable","confidence":0.0,"links":[],"pinned":false,"ttl_days":null,"evidence_refs":[]}]} with at most 10 items. Return {"memories":[]} when nothing is durable.`, toolCalls: [] }],
|
|
7
|
+
};
|
|
8
|
+
let output = "";
|
|
9
|
+
const state = provider.createRunState?.();
|
|
10
|
+
for await (const event of provider.stream(context, [], undefined, state)) {
|
|
11
|
+
if (event.type === "text_delta" && "delta" in event)
|
|
12
|
+
output += event.delta;
|
|
13
|
+
}
|
|
14
|
+
return parseExtractedMemories(output, session, scope);
|
|
15
|
+
}
|
|
16
|
+
export function parseExtractedMemories(output, session, scope) {
|
|
17
|
+
let value;
|
|
18
|
+
try {
|
|
19
|
+
value = JSON.parse(output.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.memories))
|
|
25
|
+
return [];
|
|
26
|
+
const records = [];
|
|
27
|
+
for (const raw of value.memories.slice(0, 10)) {
|
|
28
|
+
if (!raw || typeof raw !== "object")
|
|
29
|
+
continue;
|
|
30
|
+
const draft = raw;
|
|
31
|
+
const name = typeof draft.name === "string" ? draft.name.trim() : "";
|
|
32
|
+
const kind = typeof draft.kind === "string" && KINDS.has(draft.kind) ? draft.kind : undefined;
|
|
33
|
+
const content = typeof draft.content === "string" ? draft.content.trim() : "";
|
|
34
|
+
if (!name || !kind || !content)
|
|
35
|
+
continue;
|
|
36
|
+
records.push({
|
|
37
|
+
record_id: `${scope.tenant_id}:${scope.namespace}:${kind}:${name}`, scope, name, kind, content,
|
|
38
|
+
description: typeof draft.description === "string" ? draft.description.trim() : "",
|
|
39
|
+
provenance: { session_id: session.sessionId, author: "extraction", trust: "untrusted", evidence_refs: Array.isArray(draft.evidence_refs) ? draft.evidence_refs.filter((ref) => typeof ref === "string") : [] },
|
|
40
|
+
created_at: session.updatedAtMs, updated_at: session.updatedAtMs, recall_count: 0,
|
|
41
|
+
confidence: typeof draft.confidence === "number" ? Math.max(0, Math.min(1, draft.confidence)) : 0.5,
|
|
42
|
+
links: Array.isArray(draft.links) ? draft.links.filter((link) => typeof link === "string") : [],
|
|
43
|
+
pinned: draft.pinned === true,
|
|
44
|
+
...(typeof draft.ttl_days === "number" && Number.isInteger(draft.ttl_days) && draft.ttl_days > 0 ? { ttl_days: draft.ttl_days } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return records;
|
|
48
|
+
}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `InMemoryDreamStore` — a lightweight `DreamStore` implementation backed by per-agent `Map`s.
|
|
3
3
|
* WASM port of node/src/memory/in-memory-store.ts. See that file for the full design.
|
|
4
|
+
*
|
|
5
|
+
* Search returns a genuine relevance score (never stored confidence); the store bounds itself by
|
|
6
|
+
* value-ordered retention eviction (M3) and mirrors recall lifecycle and pin state (M3/M4).
|
|
4
7
|
*/
|
|
5
|
-
import type {
|
|
8
|
+
import type { DreamStore, MemoryQuery, MemoryRecall, MemoryRecallLifecycle, MemoryRecord, SessionData } from "./index.js";
|
|
9
|
+
export interface InMemoryDreamStoreOptions {
|
|
10
|
+
maxRecords?: number;
|
|
11
|
+
staleWarningDays?: number;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
6
14
|
export declare class InMemoryDreamStore implements DreamStore {
|
|
7
15
|
private readonly initialMemories;
|
|
8
|
-
private sessions;
|
|
9
16
|
private memories;
|
|
10
17
|
readonly savedSessions: SessionData[];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
private readonly maxRecords?;
|
|
19
|
+
private readonly staleWarningDays;
|
|
20
|
+
private readonly now;
|
|
21
|
+
constructor(initialMemories?: MemoryRecord[], options?: InMemoryDreamStoreOptions);
|
|
22
|
+
private recordsFor;
|
|
23
|
+
private evictToCapacity;
|
|
24
|
+
upsert(agentId: string, incoming: MemoryRecord): Promise<void>;
|
|
25
|
+
search(agentId: string, query: MemoryQuery): Promise<MemoryRecall[]>;
|
|
18
26
|
saveSession(data: SessionData): Promise<void>;
|
|
27
|
+
recordRecall(agentId: string, recalls: MemoryRecallLifecycle[]): Promise<void>;
|
|
28
|
+
setPinned(agentId: string, recordId: string, pinned: boolean): Promise<void>;
|
|
19
29
|
}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
+
import { rankMemories } from "./ranking.js";
|
|
2
|
+
import { memoryRetentionScore } from "./retention.js";
|
|
1
3
|
export class InMemoryDreamStore {
|
|
2
4
|
initialMemories;
|
|
3
|
-
sessions = new Map();
|
|
4
5
|
memories = new Map();
|
|
5
6
|
savedSessions = [];
|
|
6
|
-
|
|
7
|
+
maxRecords;
|
|
8
|
+
staleWarningDays;
|
|
9
|
+
now;
|
|
10
|
+
constructor(initialMemories = [], options = {}) {
|
|
7
11
|
this.initialMemories = initialMemories;
|
|
12
|
+
this.maxRecords = options.maxRecords;
|
|
13
|
+
this.staleWarningDays = options.staleWarningDays ?? 2;
|
|
14
|
+
this.now = options.now ?? Date.now;
|
|
8
15
|
}
|
|
9
|
-
|
|
10
|
-
const list = this.sessions.get(agentId) ?? [];
|
|
11
|
-
list.push(session);
|
|
12
|
-
this.sessions.set(agentId, list);
|
|
13
|
-
}
|
|
14
|
-
addMemories(agentId, entries) {
|
|
15
|
-
this.memories.set(agentId, [...(this.memories.get(agentId) ?? []), ...entries]);
|
|
16
|
-
}
|
|
17
|
-
async loadSessions(agentId) {
|
|
18
|
-
return this.sessions.get(agentId) ?? [];
|
|
19
|
-
}
|
|
20
|
-
async loadMemories(agentId) {
|
|
16
|
+
recordsFor(agentId) {
|
|
21
17
|
if (this.memories.has(agentId))
|
|
22
18
|
return this.memories.get(agentId);
|
|
23
19
|
if (this.initialMemories.length > 0) {
|
|
@@ -26,18 +22,67 @@ export class InMemoryDreamStore {
|
|
|
26
22
|
}
|
|
27
23
|
return [];
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
evictToCapacity(records) {
|
|
26
|
+
if (this.maxRecords === undefined || records.length <= this.maxRecords)
|
|
27
|
+
return records;
|
|
28
|
+
const nowMs = this.now();
|
|
29
|
+
const scored = records.map((record, insertionIndex) => ({
|
|
30
|
+
record,
|
|
31
|
+
insertionIndex,
|
|
32
|
+
score: record.pinned ? Number.POSITIVE_INFINITY : memoryRetentionScore(record, nowMs, this.staleWarningDays),
|
|
33
|
+
}));
|
|
34
|
+
scored.sort((a, b) => b.score - a.score || a.insertionIndex - b.insertionIndex);
|
|
35
|
+
return scored.slice(0, this.maxRecords).map(entry => entry.record);
|
|
36
|
+
}
|
|
37
|
+
async upsert(agentId, incoming) {
|
|
38
|
+
const kept = [...this.recordsFor(agentId)];
|
|
39
|
+
const index = kept.findIndex(record => record.scope.tenant_id === incoming.scope.tenant_id
|
|
40
|
+
&& record.scope.namespace === incoming.scope.namespace
|
|
41
|
+
&& record.kind === incoming.kind && record.name === incoming.name);
|
|
42
|
+
if (index >= 0)
|
|
43
|
+
kept[index] = incoming;
|
|
44
|
+
else
|
|
45
|
+
kept.push(incoming);
|
|
46
|
+
this.memories.set(agentId, this.evictToCapacity(kept));
|
|
32
47
|
}
|
|
33
|
-
async search(agentId,
|
|
34
|
-
const all =
|
|
35
|
-
|
|
48
|
+
async search(agentId, query) {
|
|
49
|
+
const all = this.recordsFor(agentId);
|
|
50
|
+
const candidates = all.filter(record => record.scope.tenant_id === query.scope.tenant_id
|
|
51
|
+
&& record.scope.namespace === query.scope.namespace
|
|
52
|
+
&& (query.kinds.length === 0 || query.kinds.includes(record.kind)));
|
|
53
|
+
const ranked = rankMemories(query.query, candidates.map((record, insertionIndex) => {
|
|
54
|
+
return {
|
|
55
|
+
value: record,
|
|
56
|
+
searchableText: `${record.name} ${record.description} ${record.content}`,
|
|
57
|
+
updatedAt: Number.isFinite(record.updated_at) ? record.updated_at : 0,
|
|
58
|
+
recallCount: record.recall_count,
|
|
59
|
+
ttlDays: record.ttl_days,
|
|
60
|
+
insertionIndex,
|
|
61
|
+
};
|
|
62
|
+
}), query.top_k, { nowMs: this.now(), staleWarningDays: this.staleWarningDays });
|
|
63
|
+
return ranked
|
|
64
|
+
.filter(hit => query.min_score === undefined || hit.score >= query.min_score)
|
|
65
|
+
.map(hit => ({ record: hit.value, score: hit.score, why: hit.why }));
|
|
36
66
|
}
|
|
37
67
|
async saveSession(data) {
|
|
38
68
|
this.savedSessions.push(data);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
69
|
+
}
|
|
70
|
+
async recordRecall(agentId, recalls) {
|
|
71
|
+
const records = this.recordsFor(agentId);
|
|
72
|
+
for (const recall of recalls) {
|
|
73
|
+
const record = records.find(candidate => candidate.record_id === recall.record_id);
|
|
74
|
+
if (record) {
|
|
75
|
+
record.recall_count = recall.recall_count;
|
|
76
|
+
record.last_recalled_at = recall.last_recalled_at;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.memories.set(agentId, records);
|
|
80
|
+
}
|
|
81
|
+
async setPinned(agentId, recordId, pinned) {
|
|
82
|
+
const records = this.recordsFor(agentId);
|
|
83
|
+
const record = records.find(candidate => candidate.record_id === recordId);
|
|
84
|
+
if (record)
|
|
85
|
+
record.pinned = pinned;
|
|
86
|
+
this.memories.set(agentId, records);
|
|
42
87
|
}
|
|
43
88
|
}
|
package/dist/memory/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare class WorkingMemory {
|
|
|
7
7
|
has(key: string): boolean;
|
|
8
8
|
}
|
|
9
9
|
export interface SessionMessage {
|
|
10
|
-
role: "user" | "assistant" | "tool";
|
|
10
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
11
11
|
content: string;
|
|
12
12
|
tokenCount?: number;
|
|
13
13
|
toolCalls?: Array<{
|
|
@@ -24,35 +24,63 @@ export interface SessionData {
|
|
|
24
24
|
createdAtMs: number;
|
|
25
25
|
updatedAtMs: number;
|
|
26
26
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
export type MemoryKind = "user" | "feedback" | "project" | "reference";
|
|
28
|
+
export type MemoryAuthor = "model" | "host" | "extraction";
|
|
29
|
+
export type MemoryTrustLevel = "untrusted" | "user_asserted" | "host_verified";
|
|
30
|
+
export interface MemoryScope {
|
|
31
|
+
tenant_id: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
}
|
|
34
|
+
export interface MemoryProvenance {
|
|
35
|
+
session_id?: string;
|
|
36
|
+
author: MemoryAuthor;
|
|
37
|
+
trust: MemoryTrustLevel;
|
|
38
|
+
evidence_refs: string[];
|
|
31
39
|
}
|
|
32
|
-
export interface
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
export interface MemoryRecord {
|
|
41
|
+
record_id: string;
|
|
42
|
+
scope: MemoryScope;
|
|
43
|
+
name: string;
|
|
44
|
+
kind: MemoryKind;
|
|
45
|
+
content: string;
|
|
46
|
+
description: string;
|
|
47
|
+
provenance: MemoryProvenance;
|
|
48
|
+
created_at: number;
|
|
49
|
+
updated_at: number;
|
|
50
|
+
last_recalled_at?: number;
|
|
51
|
+
recall_count: number;
|
|
52
|
+
confidence: number;
|
|
53
|
+
links: string[];
|
|
54
|
+
pinned: boolean;
|
|
55
|
+
ttl_days?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface MemoryRecall {
|
|
58
|
+
record: MemoryRecord;
|
|
59
|
+
score: number;
|
|
60
|
+
why: string;
|
|
37
61
|
}
|
|
38
|
-
export interface
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
export interface MemoryQuery {
|
|
63
|
+
scope: MemoryScope;
|
|
64
|
+
query: string;
|
|
65
|
+
top_k: number;
|
|
66
|
+
kinds: MemoryKind[];
|
|
67
|
+
min_score?: number;
|
|
42
68
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
/** One record's recall lifecycle, mirrored from the kernel's `memory_recalled` observation (M3). */
|
|
70
|
+
export interface MemoryRecallLifecycle {
|
|
71
|
+
record_id: string;
|
|
72
|
+
recall_count: number;
|
|
73
|
+
last_recalled_at: number;
|
|
48
74
|
}
|
|
49
75
|
export interface DreamStore {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
search(agentId: string, query: string, topK?: number): Promise<MemoryEntry[]>;
|
|
54
|
-
/** Persist a completed session for future consolidation via `Agent.dream()`. */
|
|
76
|
+
upsert(agentId: string, record: MemoryRecord): Promise<void>;
|
|
77
|
+
search(agentId: string, query: MemoryQuery): Promise<MemoryRecall[]>;
|
|
78
|
+
/** Persist a completed session before the runner's one extraction pass. */
|
|
55
79
|
saveSession(data: SessionData): Promise<void>;
|
|
80
|
+
/** M3: mirror the kernel's journaled recall lifecycle into the durable store. Optional. */
|
|
81
|
+
recordRecall?(agentId: string, recalls: MemoryRecallLifecycle[]): Promise<void>;
|
|
82
|
+
/** M4: set/clear a record's pin (exempt from retention eviction). Optional. */
|
|
83
|
+
setPinned?(agentId: string, recordId: string, pinned: boolean): Promise<void>;
|
|
56
84
|
}
|
|
57
85
|
export interface SessionStore {
|
|
58
86
|
loadSession(sessionId: string): Promise<SessionData | undefined>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RankableMemory<T> {
|
|
2
|
+
value: T;
|
|
3
|
+
searchableText: string;
|
|
4
|
+
updatedAt: number;
|
|
5
|
+
/** Times this record has been recalled — a proven-useful record ranks slightly higher. */
|
|
6
|
+
recallCount: number;
|
|
7
|
+
/** Optional day-based TTL. Records past it are discounted, not dropped (host owns hard deletion). */
|
|
8
|
+
ttlDays?: number;
|
|
9
|
+
insertionIndex: number;
|
|
10
|
+
}
|
|
11
|
+
/** One ranked hit with a genuine relevance score in [0,1] and a human-readable rationale. */
|
|
12
|
+
export interface RankedMemory<T> {
|
|
13
|
+
value: T;
|
|
14
|
+
score: number;
|
|
15
|
+
why: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RankOptions {
|
|
18
|
+
/** Wall-clock reference for the staleness discount. Omit to disable TTL/staleness scoring. */
|
|
19
|
+
nowMs?: number;
|
|
20
|
+
/** Age (days) past which a record starts losing relevance. */
|
|
21
|
+
staleWarningDays?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Rank memories without embeddings or provider calls, returning a genuine relevance score in [0,1].
|
|
25
|
+
*
|
|
26
|
+
* The score is lexical overlap (distinct query terms present, as a fraction) as the dominant term,
|
|
27
|
+
* lifted slightly by recall history and lowered by TTL/staleness. Recency and insertion order break
|
|
28
|
+
* ties. A non-empty query never returns unrelated entries (lexical overlap zero ⇒ filtered out).
|
|
29
|
+
*
|
|
30
|
+
* The score is relevance, deliberately distinct from the record's stored confidence.
|
|
31
|
+
*/
|
|
32
|
+
export declare function rankMemories<T>(query: string, candidates: Array<RankableMemory<T>>, topK: number, opts?: RankOptions): Array<RankedMemory<T>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function terms(text) {
|
|
2
|
+
const result = new Set();
|
|
3
|
+
for (const segment of text.toLocaleLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []) {
|
|
4
|
+
result.add(segment);
|
|
5
|
+
if (/\p{Script=Han}/u.test(segment)) {
|
|
6
|
+
const characters = [...segment];
|
|
7
|
+
for (let index = 0; index + 1 < characters.length; index++) {
|
|
8
|
+
result.add(characters[index] + characters[index + 1]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
const DAY_MS = 86_400_000;
|
|
15
|
+
/** Staleness discount in [0,1): grows once a record is older than the warning window, and jumps
|
|
16
|
+
* past its TTL. Clock-based, so it lives host-side (the kernel owns no wall clock). */
|
|
17
|
+
function stalenessPenalty(updatedAt, ttlDays, opts) {
|
|
18
|
+
if (opts.nowMs === undefined)
|
|
19
|
+
return 0;
|
|
20
|
+
const ageDays = Math.max(0, (opts.nowMs - updatedAt) / DAY_MS);
|
|
21
|
+
let penalty = 0;
|
|
22
|
+
if (opts.staleWarningDays !== undefined && ageDays > opts.staleWarningDays) {
|
|
23
|
+
penalty += Math.min(0.3, 0.05 * (ageDays - opts.staleWarningDays));
|
|
24
|
+
}
|
|
25
|
+
if (ttlDays !== undefined && ageDays > ttlDays) {
|
|
26
|
+
penalty += 0.4;
|
|
27
|
+
}
|
|
28
|
+
return Math.min(0.9, penalty);
|
|
29
|
+
}
|
|
30
|
+
/** Small proven-usefulness boost from recall history: log-shaped, capped so it never overturns a
|
|
31
|
+
* clearly more lexically relevant record. */
|
|
32
|
+
function recallBoost(recallCount) {
|
|
33
|
+
if (recallCount <= 0)
|
|
34
|
+
return 0;
|
|
35
|
+
return Math.min(0.15, 0.05 * Math.log2(1 + recallCount));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Rank memories without embeddings or provider calls, returning a genuine relevance score in [0,1].
|
|
39
|
+
*
|
|
40
|
+
* The score is lexical overlap (distinct query terms present, as a fraction) as the dominant term,
|
|
41
|
+
* lifted slightly by recall history and lowered by TTL/staleness. Recency and insertion order break
|
|
42
|
+
* ties. A non-empty query never returns unrelated entries (lexical overlap zero ⇒ filtered out).
|
|
43
|
+
*
|
|
44
|
+
* The score is relevance, deliberately distinct from the record's stored confidence.
|
|
45
|
+
*/
|
|
46
|
+
export function rankMemories(query, candidates, topK, opts = {}) {
|
|
47
|
+
const queryTerms = terms(query);
|
|
48
|
+
const limit = Math.max(0, Math.floor(topK));
|
|
49
|
+
if (limit === 0)
|
|
50
|
+
return [];
|
|
51
|
+
return candidates
|
|
52
|
+
.map(candidate => {
|
|
53
|
+
const candidateTerms = terms(candidate.searchableText);
|
|
54
|
+
let lexicalMatches = 0;
|
|
55
|
+
for (const term of queryTerms) {
|
|
56
|
+
if (candidateTerms.has(term))
|
|
57
|
+
lexicalMatches++;
|
|
58
|
+
}
|
|
59
|
+
const lexicalFraction = queryTerms.size === 0 ? 0 : lexicalMatches / queryTerms.size;
|
|
60
|
+
const penalty = stalenessPenalty(candidate.updatedAt, candidate.ttlDays, opts);
|
|
61
|
+
const boost = recallBoost(candidate.recallCount);
|
|
62
|
+
const score = Math.max(0, Math.min(1, lexicalFraction * 0.85 + boost - penalty));
|
|
63
|
+
const why = queryTerms.size === 0
|
|
64
|
+
? "no query terms; insertion order"
|
|
65
|
+
: `lexical ${lexicalMatches}/${queryTerms.size}`
|
|
66
|
+
+ (boost > 0 ? `, recall×${candidate.recallCount}` : "")
|
|
67
|
+
+ (penalty > 0 ? `, stale −${penalty.toFixed(2)}` : "");
|
|
68
|
+
return { ...candidate, lexicalMatches, score, why };
|
|
69
|
+
})
|
|
70
|
+
.filter(candidate => queryTerms.size === 0 || candidate.lexicalMatches > 0)
|
|
71
|
+
.sort((a, b) => b.score - a.score
|
|
72
|
+
|| b.lexicalMatches - a.lexicalMatches
|
|
73
|
+
|| b.updatedAt - a.updatedAt
|
|
74
|
+
|| a.insertionIndex - b.insertionIndex)
|
|
75
|
+
.slice(0, limit)
|
|
76
|
+
.map(candidate => ({ value: candidate.value, score: candidate.score, why: candidate.why }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side mirror of the kernel's shared retention vocabulary (`crates/.../mm/value.rs` +
|
|
3
|
+
* `memory_retention_score`). The durable store owns the full cross-session record set, so it — not
|
|
4
|
+
* the kernel — enforces the capacity bound; it must rank by the same integer "value" definition so
|
|
5
|
+
* memory and context knowledge agree on what is worth keeping.
|
|
6
|
+
*
|
|
7
|
+
* Integer terms (magnitudes well within 2^53, so JS number math is exact). The turn-based recency
|
|
8
|
+
* term is omitted here: the host store has no turn counter, so a record's recall_count, kind,
|
|
9
|
+
* confidence, size, and (clock-based) staleness drive the ordering. `parity` tests pin this against
|
|
10
|
+
* the Rust reference for the terms both compute.
|
|
11
|
+
*/
|
|
12
|
+
import type { MemoryRecord } from "./index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic retention score for a durable record. Higher is retained first;
|
|
15
|
+
* `Number.POSITIVE_INFINITY` is reserved for pins (the kernel uses `i64::MAX`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function memoryRetentionScore(record: MemoryRecord, nowMs: number, staleWarningDays: number): number;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DAY_MS = 86_400_000;
|
|
2
|
+
const KIND_WEIGHT = {
|
|
3
|
+
user: 1_600,
|
|
4
|
+
feedback: 1_800,
|
|
5
|
+
project: 1_400,
|
|
6
|
+
reference: 1_200,
|
|
7
|
+
};
|
|
8
|
+
/** floor(log2(1 + n)) — the kernel's stable ln(1+n) proxy for usage. */
|
|
9
|
+
function usageBucket(useCount) {
|
|
10
|
+
if (useCount <= 0)
|
|
11
|
+
return 0;
|
|
12
|
+
return Math.floor(Math.log2(useCount + 1));
|
|
13
|
+
}
|
|
14
|
+
/** Day-based staleness discount in parts-per-million, matching the store's recall-ranking penalty
|
|
15
|
+
* band but expressed on the kernel's ppm scale. */
|
|
16
|
+
function staleDiscountPpm(updatedAt, ttlDays, nowMs, staleWarningDays) {
|
|
17
|
+
const ageDays = Math.max(0, (nowMs - updatedAt) / DAY_MS);
|
|
18
|
+
let ppm = 0;
|
|
19
|
+
if (ageDays > staleWarningDays) {
|
|
20
|
+
ppm += Math.min(300_000, 50_000 * (ageDays - staleWarningDays));
|
|
21
|
+
}
|
|
22
|
+
if (ttlDays !== undefined && ageDays > ttlDays) {
|
|
23
|
+
ppm += 400_000;
|
|
24
|
+
}
|
|
25
|
+
return Math.min(1_000_000, Math.floor(ppm));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Deterministic retention score for a durable record. Higher is retained first;
|
|
29
|
+
* `Number.POSITIVE_INFINITY` is reserved for pins (the kernel uses `i64::MAX`).
|
|
30
|
+
*/
|
|
31
|
+
export function memoryRetentionScore(record, nowMs, staleWarningDays) {
|
|
32
|
+
if (record.pinned)
|
|
33
|
+
return Number.POSITIVE_INFINITY;
|
|
34
|
+
const usage = usageBucket(record.recall_count) * 8_192;
|
|
35
|
+
// Recency (turn-based in the kernel) is unavailable host-side; omitted deterministically.
|
|
36
|
+
const kind = KIND_WEIGHT[record.kind] ?? 0;
|
|
37
|
+
const confidencePpm = Math.min(1_000_000, Math.floor(Math.max(0, Math.min(1, record.confidence)) * 1_000_000));
|
|
38
|
+
const confidence = Math.floor(confidencePpm / 250);
|
|
39
|
+
const stalePpm = staleDiscountPpm(record.updated_at, record.ttl_days, nowMs, staleWarningDays);
|
|
40
|
+
const staleness = Math.floor(stalePpm / 125);
|
|
41
|
+
// 4-bytes-per-token proxy, matching the kernel's content.len()/4.
|
|
42
|
+
const tokens = Math.min(0xffffffff, Math.floor(byteLength(record.content) / 4));
|
|
43
|
+
const size = tokens * 4;
|
|
44
|
+
return usage + kind + confidence - staleness - size;
|
|
45
|
+
}
|
|
46
|
+
function byteLength(text) {
|
|
47
|
+
// Match Rust's content.len() (UTF-8 bytes) without pulling in Buffer for portability.
|
|
48
|
+
let bytes = 0;
|
|
49
|
+
for (const ch of text) {
|
|
50
|
+
const code = ch.codePointAt(0);
|
|
51
|
+
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
|
|
52
|
+
}
|
|
53
|
+
return bytes;
|
|
54
|
+
}
|
package/dist/providers/base.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Message, RenderedContext } from "../types.js";
|
|
2
2
|
import { assistantReplayKey } from "../runtime/provider-replay.js";
|
|
3
|
+
export declare class UnsupportedModalityError extends Error {
|
|
4
|
+
readonly modality: string;
|
|
5
|
+
readonly provider: string;
|
|
6
|
+
constructor(modality: string, provider: string);
|
|
7
|
+
}
|
|
3
8
|
/** History turns with the volatile State turn appended as the latest turn
|
|
4
9
|
* (OpenAI), keeping the history a stable cacheable prefix. Anthropic appends it
|
|
5
10
|
* after the cache breakpoint. Absent on un-rebuilt bindings — then the state is
|
package/dist/providers/base.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { assistantReplayKey } from "../runtime/provider-replay.js";
|
|
2
|
+
export class UnsupportedModalityError extends Error {
|
|
3
|
+
modality;
|
|
4
|
+
provider;
|
|
5
|
+
constructor(modality, provider) {
|
|
6
|
+
super(`UnsupportedModality: ${modality} is not supported by ${provider}`);
|
|
7
|
+
this.name = "UnsupportedModalityError";
|
|
8
|
+
this.modality = modality;
|
|
9
|
+
this.provider = provider;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
function parseToolArguments(args) {
|
|
3
13
|
try {
|
|
4
14
|
return JSON.parse(args || "{}");
|
|
@@ -7,6 +17,16 @@ function parseToolArguments(args) {
|
|
|
7
17
|
return {};
|
|
8
18
|
}
|
|
9
19
|
}
|
|
20
|
+
/** Map an audio MIME type to OpenAI's `input_audio.format` (accepts "mp3" | "wav").
|
|
21
|
+
* `audio/mpeg` must become "mp3", not the raw "mpeg" subtype. */
|
|
22
|
+
function openaiAudioFormat(mediaType) {
|
|
23
|
+
const sub = (mediaType ?? "audio/wav").split("/")[1]?.toLowerCase() ?? "wav";
|
|
24
|
+
if (sub === "mpeg" || sub === "mp3")
|
|
25
|
+
return "mp3";
|
|
26
|
+
if (sub === "wav" || sub === "wave" || sub === "x-wav")
|
|
27
|
+
return "wav";
|
|
28
|
+
return sub;
|
|
29
|
+
}
|
|
10
30
|
/** Multimodal: OpenAI content blocks from contentParts (text + image). */
|
|
11
31
|
function openAIPartsContent(parts) {
|
|
12
32
|
return parts.map(p => {
|
|
@@ -14,6 +34,15 @@ function openAIPartsContent(parts) {
|
|
|
14
34
|
const url = p.data ? `data:${p.mediaType ?? "image/png"};base64,${p.data}` : (p.url ?? "");
|
|
15
35
|
return { type: "image_url", image_url: { url, ...(p.detail ? { detail: p.detail } : {}) } };
|
|
16
36
|
}
|
|
37
|
+
if (p.type === "audio") {
|
|
38
|
+
// OpenAI audio has no URL form — a part without base64 data cannot be sent.
|
|
39
|
+
if (!p.data)
|
|
40
|
+
throw new UnsupportedModalityError("audio (no data)", "openai");
|
|
41
|
+
return {
|
|
42
|
+
type: "input_audio",
|
|
43
|
+
input_audio: { data: p.data, format: openaiAudioFormat(p.mediaType) },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
17
46
|
return { type: "text", text: p.text ?? p.output ?? "" };
|
|
18
47
|
});
|
|
19
48
|
}
|
|
@@ -26,6 +55,9 @@ function anthropicPartsContent(parts) {
|
|
|
26
55
|
: { type: "url", url: p.url ?? "" };
|
|
27
56
|
return { type: "image", source };
|
|
28
57
|
}
|
|
58
|
+
if (p.type === "audio") {
|
|
59
|
+
throw new UnsupportedModalityError("audio", "anthropic");
|
|
60
|
+
}
|
|
29
61
|
return { type: "text", text: p.text ?? p.output ?? "" };
|
|
30
62
|
});
|
|
31
63
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const CONTEXT_POLICY_VERSION: 1;
|
|
2
|
+
export declare const PPM_SCALE: 1000000;
|
|
3
|
+
export interface ContextPressureThresholdsV1 {
|
|
4
|
+
snip: number;
|
|
5
|
+
micro: number;
|
|
6
|
+
collapse: number;
|
|
7
|
+
auto: number;
|
|
8
|
+
renewal: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ContextPolicyV1 {
|
|
11
|
+
pressureThresholds: ContextPressureThresholdsV1;
|
|
12
|
+
targetAfterCompress: number;
|
|
13
|
+
preserveRecentTurns: number;
|
|
14
|
+
renewalCarryover: number;
|
|
15
|
+
collapseOldAssistantNarration: boolean;
|
|
16
|
+
idleMicroCompactMinutes: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ContextPolicyWireV1 {
|
|
19
|
+
version: typeof CONTEXT_POLICY_VERSION;
|
|
20
|
+
pressure_thresholds_ppm: ContextPressureThresholdsV1;
|
|
21
|
+
target_after_compress_ppm: number;
|
|
22
|
+
preserve_recent_turns: number;
|
|
23
|
+
renewal_carryover_ppm: number;
|
|
24
|
+
collapse_old_assistant_narration: boolean;
|
|
25
|
+
idle_micro_compact_minutes: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ContextPolicyOverridesV1 extends Partial<Omit<ContextPolicyV1, "pressureThresholds">> {
|
|
28
|
+
pressureThresholds?: Partial<ContextPressureThresholdsV1>;
|
|
29
|
+
}
|
|
30
|
+
export declare const DEFAULT_CONTEXT_POLICY_V1: Readonly<ContextPolicyV1>;
|
|
31
|
+
/** Resolve ergonomic partial SDK options into one complete, atomically validated policy. */
|
|
32
|
+
export declare function contextPolicyV1(overrides?: ContextPolicyOverridesV1): ContextPolicyV1;
|
|
33
|
+
/** Convert the public ratio-based policy to the canonical integer-only ABI wire shape. */
|
|
34
|
+
export declare function normalizeContextPolicyV1(policy: ContextPolicyV1): ContextPolicyWireV1;
|
|
35
|
+
export declare function ratioToPpm(value: number, field?: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const CONTEXT_POLICY_VERSION = 1;
|
|
2
|
+
export const PPM_SCALE = 1_000_000;
|
|
3
|
+
export const DEFAULT_CONTEXT_POLICY_V1 = Object.freeze({
|
|
4
|
+
pressureThresholds: Object.freeze({ snip: 0.70, micro: 0.80, collapse: 0.90, auto: 0.95, renewal: 0.98 }),
|
|
5
|
+
targetAfterCompress: 0.65,
|
|
6
|
+
preserveRecentTurns: 2,
|
|
7
|
+
renewalCarryover: 0.05,
|
|
8
|
+
collapseOldAssistantNarration: true,
|
|
9
|
+
idleMicroCompactMinutes: 60,
|
|
10
|
+
});
|
|
11
|
+
/** Resolve ergonomic partial SDK options into one complete, atomically validated policy. */
|
|
12
|
+
export function contextPolicyV1(overrides = {}) {
|
|
13
|
+
const policy = {
|
|
14
|
+
...DEFAULT_CONTEXT_POLICY_V1,
|
|
15
|
+
...overrides,
|
|
16
|
+
pressureThresholds: {
|
|
17
|
+
...DEFAULT_CONTEXT_POLICY_V1.pressureThresholds,
|
|
18
|
+
...overrides.pressureThresholds,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
normalizeContextPolicyV1(policy);
|
|
22
|
+
return policy;
|
|
23
|
+
}
|
|
24
|
+
/** Convert the public ratio-based policy to the canonical integer-only ABI wire shape. */
|
|
25
|
+
export function normalizeContextPolicyV1(policy) {
|
|
26
|
+
const pressure_thresholds_ppm = {
|
|
27
|
+
snip: ratioToPpm(policy.pressureThresholds.snip, "pressureThresholds.snip"),
|
|
28
|
+
micro: ratioToPpm(policy.pressureThresholds.micro, "pressureThresholds.micro"),
|
|
29
|
+
collapse: ratioToPpm(policy.pressureThresholds.collapse, "pressureThresholds.collapse"),
|
|
30
|
+
auto: ratioToPpm(policy.pressureThresholds.auto, "pressureThresholds.auto"),
|
|
31
|
+
renewal: ratioToPpm(policy.pressureThresholds.renewal, "pressureThresholds.renewal"),
|
|
32
|
+
};
|
|
33
|
+
const ordered = Object.values(pressure_thresholds_ppm);
|
|
34
|
+
if (!ordered.every((value, index) => index === 0 || ordered[index - 1] < value)) {
|
|
35
|
+
throw new RangeError("context pressure thresholds must satisfy snip < micro < collapse < auto < renewal");
|
|
36
|
+
}
|
|
37
|
+
const target_after_compress_ppm = ratioToPpm(policy.targetAfterCompress, "targetAfterCompress");
|
|
38
|
+
if (target_after_compress_ppm >= pressure_thresholds_ppm.snip) {
|
|
39
|
+
throw new RangeError("targetAfterCompress must be lower than the snip threshold");
|
|
40
|
+
}
|
|
41
|
+
assertIntegerAtLeast(policy.preserveRecentTurns, 1, "preserveRecentTurns");
|
|
42
|
+
assertIntegerAtLeast(policy.idleMicroCompactMinutes, 0, "idleMicroCompactMinutes");
|
|
43
|
+
if (typeof policy.collapseOldAssistantNarration !== "boolean") {
|
|
44
|
+
throw new TypeError("collapseOldAssistantNarration must be boolean");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
version: CONTEXT_POLICY_VERSION,
|
|
48
|
+
pressure_thresholds_ppm,
|
|
49
|
+
target_after_compress_ppm,
|
|
50
|
+
preserve_recent_turns: policy.preserveRecentTurns,
|
|
51
|
+
renewal_carryover_ppm: ratioToPpm(policy.renewalCarryover, "renewalCarryover"),
|
|
52
|
+
collapse_old_assistant_narration: policy.collapseOldAssistantNarration,
|
|
53
|
+
idle_micro_compact_minutes: policy.idleMicroCompactMinutes,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function ratioToPpm(value, field = "ratio") {
|
|
57
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
58
|
+
throw new RangeError(`${field} must be a finite number between 0 and 1`);
|
|
59
|
+
}
|
|
60
|
+
return Math.floor(value * PPM_SCALE + 0.5);
|
|
61
|
+
}
|
|
62
|
+
function assertIntegerAtLeast(value, minimum, field) {
|
|
63
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
64
|
+
throw new RangeError(`${field} must be a safe integer >= ${minimum}`);
|
|
65
|
+
}
|
|
66
|
+
}
|