@rine-kit/agent-memory 0.0.0-stage → 0.1.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/dist/context/budget.d.ts +55 -0
- package/dist/context/budget.js +103 -0
- package/dist/context/checkpoint.d.ts +24 -0
- package/dist/context/checkpoint.js +67 -0
- package/dist/contracts.d.ts +160 -0
- package/dist/contracts.js +5 -0
- package/dist/dreaming/candidates.d.ts +26 -0
- package/dist/dreaming/candidates.js +53 -0
- package/dist/dreaming/deep.d.ts +27 -0
- package/dist/dreaming/deep.js +63 -0
- package/dist/dreaming/runner.d.ts +16 -0
- package/dist/dreaming/runner.js +54 -0
- package/dist/dreaming/schedule.d.ts +3 -0
- package/dist/dreaming/schedule.js +72 -0
- package/dist/dreaming/scheduler.d.ts +22 -0
- package/dist/dreaming/scheduler.js +96 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +1 -0
- package/dist/ports/index.d.ts +42 -0
- package/dist/ports/index.js +3 -0
- package/dist/recall/fusion.d.ts +2 -0
- package/dist/recall/fusion.js +96 -0
- package/dist/recall/search.d.ts +12 -0
- package/dist/recall/search.js +81 -0
- package/dist/recall/service.d.ts +26 -0
- package/dist/recall/service.js +146 -0
- package/dist/runtime.d.ts +38 -0
- package/dist/runtime.js +86 -0
- package/dist/signals.d.ts +9 -0
- package/dist/signals.js +37 -0
- package/dist/write/conflicts.d.ts +9 -0
- package/dist/write/conflicts.js +38 -0
- package/dist/write/service.d.ts +18 -0
- package/dist/write/service.js +85 -0
- package/package.json +32 -3
- package/README.md +0 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createMemoryUsageSignals } from '../signals.js';
|
|
2
|
+
import { fuseMemoryCandidates } from './fusion.js';
|
|
3
|
+
const DEFAULT_TOP_K = 6;
|
|
4
|
+
const DEFAULT_CANDIDATE_LIMIT = 20;
|
|
5
|
+
const DEFAULT_SEMANTIC_THRESHOLD = 0.72;
|
|
6
|
+
const DEFAULT_MINIMUM_SCORE = 0.005;
|
|
7
|
+
function fallbackContext(candidates) {
|
|
8
|
+
if (!candidates.length)
|
|
9
|
+
return null;
|
|
10
|
+
return JSON.stringify(candidates.map((candidate) => ({
|
|
11
|
+
key: candidate.key,
|
|
12
|
+
content: candidate.content,
|
|
13
|
+
recordedAt: candidate.updatedAt,
|
|
14
|
+
source: candidate.sourceKind,
|
|
15
|
+
revision: candidate.revision,
|
|
16
|
+
})));
|
|
17
|
+
}
|
|
18
|
+
function settled(result, fallback) {
|
|
19
|
+
return result.status === 'fulfilled' ? result.value : fallback;
|
|
20
|
+
}
|
|
21
|
+
function candidateSourcesFailed(exactFailed, lexicalFailed, semanticFailed, semanticReason) {
|
|
22
|
+
return exactFailed && lexicalFailed && (semanticFailed || semanticReason !== null);
|
|
23
|
+
}
|
|
24
|
+
/** Generic authorization-fenced recall orchestration; the host supplies scoped storage and rendering. */
|
|
25
|
+
export class MemoryRecallService {
|
|
26
|
+
storage;
|
|
27
|
+
options;
|
|
28
|
+
topK;
|
|
29
|
+
candidateLimit;
|
|
30
|
+
semanticSimilarityThreshold;
|
|
31
|
+
minimumScore;
|
|
32
|
+
now;
|
|
33
|
+
constructor(storage, options = {}) {
|
|
34
|
+
this.storage = storage;
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.topK = options.topK ?? DEFAULT_TOP_K;
|
|
37
|
+
this.candidateLimit = options.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT;
|
|
38
|
+
this.semanticSimilarityThreshold =
|
|
39
|
+
options.semanticSimilarityThreshold ?? DEFAULT_SEMANTIC_THRESHOLD;
|
|
40
|
+
this.minimumScore = options.minimumScore ?? DEFAULT_MINIMUM_SCORE;
|
|
41
|
+
this.now = options.now ?? Date.now;
|
|
42
|
+
if (!Number.isSafeInteger(this.topK) ||
|
|
43
|
+
this.topK < 5 ||
|
|
44
|
+
this.topK > 8 ||
|
|
45
|
+
!Number.isSafeInteger(this.candidateLimit) ||
|
|
46
|
+
this.candidateLimit < this.topK ||
|
|
47
|
+
this.candidateLimit > 24 ||
|
|
48
|
+
!Number.isFinite(this.semanticSimilarityThreshold) ||
|
|
49
|
+
this.semanticSimilarityThreshold < -1 ||
|
|
50
|
+
this.semanticSimilarityThreshold > 1 ||
|
|
51
|
+
!Number.isFinite(this.minimumScore) ||
|
|
52
|
+
this.minimumScore < 0 ||
|
|
53
|
+
this.minimumScore > 1) {
|
|
54
|
+
throw new Error('memory_retrieval_limits_invalid');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
report(startedAt, result, counts) {
|
|
58
|
+
try {
|
|
59
|
+
this.options.onMetric?.({
|
|
60
|
+
mode: result.mode,
|
|
61
|
+
reason: result.reason,
|
|
62
|
+
durationMs: Math.max(0, this.now() - startedAt),
|
|
63
|
+
candidateCount: result.candidates.length,
|
|
64
|
+
exactCount: counts.exact,
|
|
65
|
+
lexicalCount: counts.lexical,
|
|
66
|
+
semanticCount: counts.semantic,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* metrics never change recall */
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async recall(authorization, query, limit = this.candidateLimit) {
|
|
75
|
+
const startedAt = this.now();
|
|
76
|
+
const counts = { exact: 0, lexical: 0, semantic: 0 };
|
|
77
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
78
|
+
return this.report(startedAt, { mode: 'disabled', reason: 'memory_store_unavailable', candidates: [], context: null }, counts);
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isSafeInteger(limit) || limit < this.topK || limit > 24) {
|
|
81
|
+
return this.report(startedAt, { mode: 'disabled', reason: 'memory_store_unavailable', candidates: [], context: null }, counts);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return await this.storage.withAuthorizedSearch(authorization, async (searches) => {
|
|
85
|
+
const results = await Promise.allSettled([
|
|
86
|
+
searches.exact(query, limit),
|
|
87
|
+
searches.lexical(query, limit),
|
|
88
|
+
searches.semantic(query, limit),
|
|
89
|
+
]);
|
|
90
|
+
const exactResult = results[0];
|
|
91
|
+
const lexicalResult = results[1];
|
|
92
|
+
const semanticResult = results[2];
|
|
93
|
+
const exact = settled(exactResult, []);
|
|
94
|
+
const lexical = settled(lexicalResult, []);
|
|
95
|
+
const semantic = settled(semanticResult, {
|
|
96
|
+
matches: [],
|
|
97
|
+
reason: 'semantic_unavailable',
|
|
98
|
+
});
|
|
99
|
+
counts.exact = exact.length;
|
|
100
|
+
counts.lexical = lexical.length;
|
|
101
|
+
counts.semantic = semantic.matches.length;
|
|
102
|
+
const candidates = fuseMemoryCandidates(query, exact, lexical, semantic.matches, {
|
|
103
|
+
topK: this.topK,
|
|
104
|
+
semanticSimilarityThreshold: this.semanticSimilarityThreshold,
|
|
105
|
+
minimumScore: this.minimumScore,
|
|
106
|
+
});
|
|
107
|
+
const allSourcesFailed = candidateSourcesFailed(exactResult.status === 'rejected', lexicalResult.status === 'rejected', semanticResult.status === 'rejected', semantic.reason);
|
|
108
|
+
const eitherCandidateSourceFailed = exactResult.status === 'rejected' || lexicalResult.status === 'rejected';
|
|
109
|
+
const mode = allSourcesFailed
|
|
110
|
+
? 'disabled'
|
|
111
|
+
: semantic.reason === null
|
|
112
|
+
? 'hybrid'
|
|
113
|
+
: 'lexical_only';
|
|
114
|
+
const reason = allSourcesFailed
|
|
115
|
+
? 'memory_store_unavailable'
|
|
116
|
+
: eitherCandidateSourceFailed
|
|
117
|
+
? 'memory_candidate_source_unavailable'
|
|
118
|
+
: semantic.reason;
|
|
119
|
+
const visible = mode === 'disabled' ? [] : candidates;
|
|
120
|
+
const result = {
|
|
121
|
+
mode,
|
|
122
|
+
reason,
|
|
123
|
+
candidates: visible,
|
|
124
|
+
context: visible.length ? (this.options.renderContext ?? fallbackContext)(visible) : null,
|
|
125
|
+
};
|
|
126
|
+
if (result.candidates.length) {
|
|
127
|
+
try {
|
|
128
|
+
const signals = createMemoryUsageSignals(query, result.candidates);
|
|
129
|
+
await this.storage.recordUsageSignals(authorization, 'recalled', signals);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* usage evidence must never change recall */
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return this.report(startedAt, result, counts);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return this.report(startedAt, { mode: 'disabled', reason: 'memory_scope_unavailable', candidates: [], context: null }, counts);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async recordUsed(authorization, query, candidates) {
|
|
143
|
+
const signals = createMemoryUsageSignals(query, candidates);
|
|
144
|
+
await this.storage.recordUsageSignals(authorization, 'used', signals);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DreamingReport, ExplicitMemoryWrite, MemoryFeatureFlags, MemoryForgetRequest, MemoryHealth, MemoryObservation, MemoryRecallRequest, MemoryRecallResult, MemoryRetrievedCandidate, MemoryWriteResult } from './contracts.js';
|
|
2
|
+
import type { AgentMemoryPlugin } from './plugin.js';
|
|
3
|
+
export interface MemoryRuntimeRecallPort<TAuthorization> {
|
|
4
|
+
recall(authorization: TAuthorization, request: MemoryRecallRequest): Promise<MemoryRecallResult>;
|
|
5
|
+
recordUsed(authorization: TAuthorization, query: string, candidates: readonly MemoryRetrievedCandidate[]): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface MemoryRuntimeWritePort<TAuthorization> {
|
|
8
|
+
remember(authorization: TAuthorization, write: ExplicitMemoryWrite): Promise<MemoryWriteResult>;
|
|
9
|
+
forget(authorization: TAuthorization, request: MemoryForgetRequest): Promise<MemoryWriteResult>;
|
|
10
|
+
}
|
|
11
|
+
export interface MemoryRuntimeAdapters<TAuthorization> {
|
|
12
|
+
observe: ((authorization: TAuthorization, observation: MemoryObservation) => Promise<void>) | null;
|
|
13
|
+
recall: MemoryRuntimeRecallPort<TAuthorization> | null;
|
|
14
|
+
writes: MemoryRuntimeWritePort<TAuthorization> | null;
|
|
15
|
+
runDreaming: ((signal: AbortSignal) => Promise<DreamingReport>) | null;
|
|
16
|
+
}
|
|
17
|
+
export interface AgentMemoryRuntimeOptions<TAuthorization> {
|
|
18
|
+
features?: Partial<MemoryFeatureFlags>;
|
|
19
|
+
adapters: MemoryRuntimeAdapters<TAuthorization>;
|
|
20
|
+
now?: () => Date;
|
|
21
|
+
}
|
|
22
|
+
/** Generic feature-gated memory runtime; all identity and persistence remain in host adapters. */
|
|
23
|
+
export declare class AgentMemoryRuntime<TAuthorization> implements AgentMemoryPlugin<TAuthorization> {
|
|
24
|
+
private readonly options;
|
|
25
|
+
readonly features: Readonly<MemoryFeatureFlags>;
|
|
26
|
+
private lastDreamingAt;
|
|
27
|
+
private lastFailureCode;
|
|
28
|
+
private readonly now;
|
|
29
|
+
constructor(options: AgentMemoryRuntimeOptions<TAuthorization>);
|
|
30
|
+
get autoLearnEnabled(): boolean;
|
|
31
|
+
observe(authorization: TAuthorization, observation: MemoryObservation): Promise<void>;
|
|
32
|
+
recall(authorization: TAuthorization, request: MemoryRecallRequest): Promise<MemoryRecallResult>;
|
|
33
|
+
remember(authorization: TAuthorization, write: ExplicitMemoryWrite): Promise<MemoryWriteResult>;
|
|
34
|
+
forget(authorization: TAuthorization, request: MemoryForgetRequest): Promise<MemoryWriteResult>;
|
|
35
|
+
recordUsed(authorization: TAuthorization, query: string, candidates: readonly MemoryRetrievedCandidate[]): Promise<void>;
|
|
36
|
+
runDreaming(signal?: AbortSignal): Promise<DreamingReport>;
|
|
37
|
+
health(): Promise<MemoryHealth>;
|
|
38
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MEMORY_FEATURE_DEFAULTS } from './contracts.js';
|
|
2
|
+
function skippedReport(now) {
|
|
3
|
+
return {
|
|
4
|
+
startedAt: now.toISOString(),
|
|
5
|
+
completedAt: now.toISOString(),
|
|
6
|
+
stages: [
|
|
7
|
+
{ stage: 'light', status: 'skipped', processed: 0 },
|
|
8
|
+
{ stage: 'rem', status: 'skipped', processed: 0 },
|
|
9
|
+
{ stage: 'deep', status: 'skipped', processed: 0 },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Generic feature-gated memory runtime; all identity and persistence remain in host adapters. */
|
|
14
|
+
export class AgentMemoryRuntime {
|
|
15
|
+
options;
|
|
16
|
+
features;
|
|
17
|
+
lastDreamingAt = null;
|
|
18
|
+
lastFailureCode = null;
|
|
19
|
+
now;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.features = Object.freeze({ ...MEMORY_FEATURE_DEFAULTS, ...options.features });
|
|
23
|
+
this.now = options.now ?? (() => new Date());
|
|
24
|
+
}
|
|
25
|
+
get autoLearnEnabled() {
|
|
26
|
+
return this.features.autoLearn;
|
|
27
|
+
}
|
|
28
|
+
async observe(authorization, observation) {
|
|
29
|
+
if (!this.features.autoLearn || !this.options.adapters.observe)
|
|
30
|
+
return;
|
|
31
|
+
await this.options.adapters.observe(authorization, observation);
|
|
32
|
+
}
|
|
33
|
+
async recall(authorization, request) {
|
|
34
|
+
if (!this.features.read) {
|
|
35
|
+
return { mode: 'disabled', reason: 'disabled_by_operator', candidates: [], context: null };
|
|
36
|
+
}
|
|
37
|
+
if (!this.options.adapters.recall) {
|
|
38
|
+
return {
|
|
39
|
+
mode: 'disabled',
|
|
40
|
+
reason: 'memory_store_unavailable',
|
|
41
|
+
candidates: [],
|
|
42
|
+
context: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return this.options.adapters.recall.recall(authorization, request);
|
|
46
|
+
}
|
|
47
|
+
async remember(authorization, write) {
|
|
48
|
+
if (!this.features.write || !this.options.adapters.writes)
|
|
49
|
+
return { status: 'not_found' };
|
|
50
|
+
return this.options.adapters.writes.remember(authorization, write);
|
|
51
|
+
}
|
|
52
|
+
async forget(authorization, request) {
|
|
53
|
+
if (!this.features.write || !this.options.adapters.writes)
|
|
54
|
+
return { status: 'not_found' };
|
|
55
|
+
return this.options.adapters.writes.forget(authorization, request);
|
|
56
|
+
}
|
|
57
|
+
async recordUsed(authorization, query, candidates) {
|
|
58
|
+
if (!this.features.read || !this.options.adapters.recall || !candidates.length)
|
|
59
|
+
return;
|
|
60
|
+
await this.options.adapters.recall.recordUsed(authorization, query, candidates);
|
|
61
|
+
}
|
|
62
|
+
async runDreaming(signal) {
|
|
63
|
+
if (!this.features.autoLearn)
|
|
64
|
+
return skippedReport(this.now());
|
|
65
|
+
if (!this.options.adapters.runDreaming)
|
|
66
|
+
throw new Error('memory_dreaming_adapter_unavailable');
|
|
67
|
+
try {
|
|
68
|
+
const report = await this.options.adapters.runDreaming(signal ?? new AbortController().signal);
|
|
69
|
+
this.lastDreamingAt = report.completedAt;
|
|
70
|
+
this.lastFailureCode = null;
|
|
71
|
+
return report;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.lastFailureCode = 'dreaming_run_failed';
|
|
75
|
+
throw new Error('memory_dreaming_run_failed');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async health() {
|
|
79
|
+
const missingAdapter = (this.features.read && !this.options.adapters.recall) ||
|
|
80
|
+
(this.features.write && !this.options.adapters.writes) ||
|
|
81
|
+
(this.features.autoLearn &&
|
|
82
|
+
(!this.options.adapters.observe || !this.options.adapters.runDreaming));
|
|
83
|
+
const reason = this.lastFailureCode ?? (missingAdapter ? 'memory_adapter_unavailable' : null);
|
|
84
|
+
return { status: reason ? 'degraded' : 'ready', lastDreamingAt: this.lastDreamingAt, reason };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MemoryUsageSignal } from './contracts.js';
|
|
2
|
+
export interface MemoryUsageCandidate {
|
|
3
|
+
key: string;
|
|
4
|
+
revision: number;
|
|
5
|
+
sourceEventId: string;
|
|
6
|
+
}
|
|
7
|
+
/** Hash a normalized query so recall analytics never retain the query text. */
|
|
8
|
+
export declare function memoryQueryFingerprint(query: string): string;
|
|
9
|
+
export declare function createMemoryUsageSignals(query: string, candidates: readonly MemoryUsageCandidate[]): MemoryUsageSignal[];
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const MAX_FINGERPRINT_QUERY_LENGTH = 8_000;
|
|
3
|
+
/** Hash a normalized query so recall analytics never retain the query text. */
|
|
4
|
+
export function memoryQueryFingerprint(query) {
|
|
5
|
+
if (typeof query !== 'string' || query.length > MAX_FINGERPRINT_QUERY_LENGTH) {
|
|
6
|
+
throw new Error('memory_query_fingerprint_input_invalid');
|
|
7
|
+
}
|
|
8
|
+
const normalized = query.normalize('NFKC').trim().replace(/\s+/gu, ' ').toLowerCase();
|
|
9
|
+
if (!normalized)
|
|
10
|
+
throw new Error('memory_query_fingerprint_input_invalid');
|
|
11
|
+
return createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
12
|
+
}
|
|
13
|
+
export function createMemoryUsageSignals(query, candidates) {
|
|
14
|
+
const queryFingerprint = memoryQueryFingerprint(query);
|
|
15
|
+
const unique = new Map();
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (!candidate ||
|
|
18
|
+
typeof candidate.key !== 'string' ||
|
|
19
|
+
!candidate.key.trim() ||
|
|
20
|
+
candidate.key.length > 96 ||
|
|
21
|
+
!Number.isSafeInteger(candidate.revision) ||
|
|
22
|
+
candidate.revision < 1 ||
|
|
23
|
+
typeof candidate.sourceEventId !== 'string' ||
|
|
24
|
+
!candidate.sourceEventId.trim() ||
|
|
25
|
+
candidate.sourceEventId.length > 256) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const signal = {
|
|
29
|
+
key: candidate.key,
|
|
30
|
+
revision: candidate.revision,
|
|
31
|
+
sourceEventId: candidate.sourceEventId,
|
|
32
|
+
queryFingerprint,
|
|
33
|
+
};
|
|
34
|
+
unique.set(JSON.stringify([signal.key, signal.revision, signal.sourceEventId]), signal);
|
|
35
|
+
}
|
|
36
|
+
return [...unique.values()];
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MemoryEntry } from '../contracts.js';
|
|
2
|
+
export interface MemoryConflictCandidate {
|
|
3
|
+
entry: MemoryEntry;
|
|
4
|
+
overlap: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function memoryTokenOverlap(left: string, right: string): number;
|
|
7
|
+
/** Finds likely same-fact conflicts while excluding revisions of the requested key. */
|
|
8
|
+
export declare function findMemoryConflicts(key: string, content: string, entries: readonly MemoryEntry[], minimumOverlap?: number): MemoryConflictCandidate[];
|
|
9
|
+
export declare function equivalentMemoryContent(left: string, right: string, minimumOverlap?: number): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { memorySearchTokens, normalizeMemoryKey } from '../recall/search.js';
|
|
2
|
+
export function memoryTokenOverlap(left, right) {
|
|
3
|
+
const leftTokens = new Set(memorySearchTokens(left));
|
|
4
|
+
const rightTokens = new Set(memorySearchTokens(right));
|
|
5
|
+
if (!leftTokens.size || !rightTokens.size)
|
|
6
|
+
return 0;
|
|
7
|
+
let intersection = 0;
|
|
8
|
+
for (const token of leftTokens)
|
|
9
|
+
if (rightTokens.has(token))
|
|
10
|
+
intersection += 1;
|
|
11
|
+
return intersection / (leftTokens.size + rightTokens.size - intersection);
|
|
12
|
+
}
|
|
13
|
+
/** Finds likely same-fact conflicts while excluding revisions of the requested key. */
|
|
14
|
+
export function findMemoryConflicts(key, content, entries, minimumOverlap = 0.72) {
|
|
15
|
+
if (!Number.isFinite(minimumOverlap) || minimumOverlap < 0 || minimumOverlap > 1) {
|
|
16
|
+
throw new Error('memory_conflict_threshold_invalid');
|
|
17
|
+
}
|
|
18
|
+
const normalizedKey = normalizeMemoryKey(key);
|
|
19
|
+
const unique = new Map();
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (!entry || !Number.isSafeInteger(entry.revision) || entry.revision < 1)
|
|
22
|
+
continue;
|
|
23
|
+
const candidateKey = normalizeMemoryKey(entry.key);
|
|
24
|
+
if (!candidateKey || candidateKey === normalizedKey)
|
|
25
|
+
continue;
|
|
26
|
+
unique.set(`${candidateKey}\0${entry.revision}`, entry);
|
|
27
|
+
}
|
|
28
|
+
return [...unique.values()]
|
|
29
|
+
.map((entry) => ({
|
|
30
|
+
entry,
|
|
31
|
+
overlap: memoryTokenOverlap(`${key} ${content}`, `${entry.key} ${entry.content}`),
|
|
32
|
+
}))
|
|
33
|
+
.filter((candidate) => candidate.overlap >= minimumOverlap)
|
|
34
|
+
.sort((left, right) => right.overlap - left.overlap || left.entry.key.localeCompare(right.entry.key));
|
|
35
|
+
}
|
|
36
|
+
export function equivalentMemoryContent(left, right, minimumOverlap = 0.9) {
|
|
37
|
+
return memoryTokenOverlap(left, right) >= minimumOverlap;
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExplicitMemoryWrite, MemoryDraft, MemoryForgetRequest, MemoryWriteMetadata, MemoryWriteResult, MemoryWriteState } from '../contracts.js';
|
|
2
|
+
export interface MemoryWriteStoragePort<TAuthorization> {
|
|
3
|
+
getWriteState(authorization: TAuthorization, key: string): Promise<MemoryWriteState | null>;
|
|
4
|
+
create(authorization: TAuthorization, draft: MemoryDraft, metadata: MemoryWriteMetadata): Promise<'created' | 'already_exists'>;
|
|
5
|
+
revise(authorization: TAuthorization, draft: MemoryDraft, expectedRevision: number, metadata: MemoryWriteMetadata): Promise<boolean>;
|
|
6
|
+
rememberAgain(authorization: TAuthorization, draft: MemoryDraft, expectedRevision: number, metadata: MemoryWriteMetadata): Promise<boolean>;
|
|
7
|
+
forget(authorization: TAuthorization, key: string, expectedRevision: number): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
export interface MemoryWriteContext {
|
|
10
|
+
sourceEventId: string;
|
|
11
|
+
}
|
|
12
|
+
/** Generic explicit write lifecycle; the host port enforces its identity and storage fences. */
|
|
13
|
+
export declare class MemoryWriteService<TAuthorization> {
|
|
14
|
+
private readonly storage;
|
|
15
|
+
constructor(storage: MemoryWriteStoragePort<TAuthorization>);
|
|
16
|
+
remember(authorization: TAuthorization, write: ExplicitMemoryWrite, context: MemoryWriteContext, metadata?: MemoryWriteMetadata): Promise<MemoryWriteResult>;
|
|
17
|
+
forget(authorization: TAuthorization, request: MemoryForgetRequest, context: MemoryWriteContext): Promise<MemoryWriteResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { canonicalizeMemoryKey, validateMemoryDraft } from '../recall/search.js';
|
|
2
|
+
function normalizeMetadata(input) {
|
|
3
|
+
const sourceKind = input.sourceKind ?? 'user_confirmed';
|
|
4
|
+
if (!['trusted_ingress', 'user_confirmed', 'system_verified', 'unknown'].includes(sourceKind)) {
|
|
5
|
+
throw new Error('memory_source_kind_invalid');
|
|
6
|
+
}
|
|
7
|
+
const sourceReliability = input.sourceReliability ?? (sourceKind === 'user_confirmed' ? 95 : 80);
|
|
8
|
+
if (!Number.isSafeInteger(sourceReliability) ||
|
|
9
|
+
sourceReliability < 0 ||
|
|
10
|
+
sourceReliability > 100) {
|
|
11
|
+
throw new Error('memory_source_reliability_invalid');
|
|
12
|
+
}
|
|
13
|
+
let validUntil = null;
|
|
14
|
+
if (input.validUntil != null) {
|
|
15
|
+
const timestamp = Date.parse(input.validUntil);
|
|
16
|
+
if (!Number.isFinite(timestamp))
|
|
17
|
+
throw new Error('memory_valid_until_invalid');
|
|
18
|
+
validUntil = new Date(timestamp).toISOString();
|
|
19
|
+
}
|
|
20
|
+
return { sourceKind, sourceReliability, validUntil };
|
|
21
|
+
}
|
|
22
|
+
function validContext(context) {
|
|
23
|
+
return (typeof context.sourceEventId === 'string' &&
|
|
24
|
+
context.sourceEventId.trim().length > 0 &&
|
|
25
|
+
context.sourceEventId.length <= 256);
|
|
26
|
+
}
|
|
27
|
+
/** Generic explicit write lifecycle; the host port enforces its identity and storage fences. */
|
|
28
|
+
export class MemoryWriteService {
|
|
29
|
+
storage;
|
|
30
|
+
constructor(storage) {
|
|
31
|
+
this.storage = storage;
|
|
32
|
+
}
|
|
33
|
+
async remember(authorization, write, context, metadata = {}) {
|
|
34
|
+
if (!validContext(context))
|
|
35
|
+
return { status: 'not_found' };
|
|
36
|
+
const draft = validateMemoryDraft(write);
|
|
37
|
+
const normalized = normalizeMetadata({
|
|
38
|
+
sourceKind: 'user_confirmed',
|
|
39
|
+
sourceReliability: 95,
|
|
40
|
+
validUntil: write.validUntil ?? null,
|
|
41
|
+
...metadata,
|
|
42
|
+
});
|
|
43
|
+
const state = await this.storage.getWriteState(authorization, draft.key);
|
|
44
|
+
if (write.operation === 'correct') {
|
|
45
|
+
if (!state || state.revoked || write.expectedRevision !== state.revision)
|
|
46
|
+
return { status: 'not_found' };
|
|
47
|
+
return (await this.storage.revise(authorization, draft, state.revision, normalized))
|
|
48
|
+
? { status: 'revised' }
|
|
49
|
+
: { status: 'not_found' };
|
|
50
|
+
}
|
|
51
|
+
if (state?.revoked) {
|
|
52
|
+
if (write.expectedRevision !== state.revision)
|
|
53
|
+
return { status: 'not_found' };
|
|
54
|
+
return (await this.storage.rememberAgain(authorization, draft, state.revision, normalized))
|
|
55
|
+
? { status: 'revised' }
|
|
56
|
+
: { status: 'not_found' };
|
|
57
|
+
}
|
|
58
|
+
if (state)
|
|
59
|
+
return state.content === draft.content
|
|
60
|
+
? { status: 'already_present' }
|
|
61
|
+
: { status: 'not_found' };
|
|
62
|
+
if ((await this.storage.create(authorization, draft, normalized)) === 'created')
|
|
63
|
+
return { status: 'created' };
|
|
64
|
+
const current = await this.storage.getWriteState(authorization, draft.key);
|
|
65
|
+
return current &&
|
|
66
|
+
!current.revoked &&
|
|
67
|
+
current.sourceEventId === context.sourceEventId &&
|
|
68
|
+
current.content === draft.content
|
|
69
|
+
? { status: 'created' }
|
|
70
|
+
: { status: 'not_found' };
|
|
71
|
+
}
|
|
72
|
+
async forget(authorization, request, context) {
|
|
73
|
+
if (!validContext(context))
|
|
74
|
+
return { status: 'not_found' };
|
|
75
|
+
const key = canonicalizeMemoryKey(request.key);
|
|
76
|
+
const state = await this.storage.getWriteState(authorization, key);
|
|
77
|
+
if (!state ||
|
|
78
|
+
state.revoked ||
|
|
79
|
+
(request.expectedRevision !== undefined && request.expectedRevision !== state.revision))
|
|
80
|
+
return { status: 'not_found' };
|
|
81
|
+
const forgotten = await this.storage.forget(authorization, key, state.revision);
|
|
82
|
+
const finalState = await this.storage.getWriteState(authorization, key);
|
|
83
|
+
return forgotten || finalState?.revoked ? { status: 'forgotten' } : { status: 'not_found' };
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rine-kit/agent-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Host-agnostic agent memory runtime: recall (exact/lexical/semantic fusion), explicit writes, evidence-based dreaming. Authorization stays opaque; hosts supply storage, model, embedding and clock ports.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^26.6.5",
|
|
22
|
+
"tsx": "^4.23.15",
|
|
23
|
+
"typescript": "^5.9.2"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+ssh://git@github.com/ChristmasFox/rine-kit.git",
|
|
28
|
+
"directory": "packages/agent-memory"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"test": "tsx --test tests/**/*.test.ts"
|
|
34
|
+
}
|
|
6
35
|
}
|
package/README.md
DELETED