@deepstrike/sdk 0.2.39 → 0.2.40
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 +61 -31
- package/dist/collaboration/contract.d.ts +2 -2
- package/dist/collaboration/contract.js +2 -2
- package/dist/collaboration/handoff.d.ts +2 -14
- package/dist/collaboration/handoff.js +1 -17
- package/dist/collaboration/harness.d.ts +16 -47
- package/dist/collaboration/harness.js +57 -158
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +1 -2
- package/dist/collaboration/modes/creator-verifier.js +42 -6
- package/dist/collaboration/pool.d.ts +8 -0
- package/dist/collaboration/pool.js +56 -3
- package/dist/harness/harness.d.ts +97 -97
- package/dist/harness/harness.js +190 -144
- package/dist/harness/judge.d.ts +3 -2
- package/dist/harness/judge.js +7 -3
- package/dist/harness/public.d.ts +5 -3
- package/dist/harness/public.js +3 -2
- package/dist/index.d.ts +22 -7
- package/dist/index.js +8 -1
- package/dist/kernel.d.ts +12 -39
- package/dist/memory/agent.d.ts +8 -41
- package/dist/memory/agent.js +34 -117
- package/dist/memory/extraction.d.ts +4 -0
- package/dist/memory/extraction.js +79 -0
- package/dist/memory/in-memory-store.d.ts +28 -15
- package/dist/memory/in-memory-store.js +74 -25
- package/dist/memory/protocols.d.ts +56 -56
- package/dist/memory/protocols.js +1 -0
- package/dist/memory/public.d.ts +3 -1
- package/dist/memory/public.js +1 -0
- package/dist/memory/ranking.d.ts +33 -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/os/public.d.ts +3 -3
- package/dist/os/public.js +1 -1
- package/dist/providers/base.d.ts +8 -0
- package/dist/providers/base.js +0 -0
- package/dist/providers/gemini.js +12 -0
- package/dist/providers/ollama.js +3 -1
- package/dist/providers/openai-responses.js +9 -2
- package/dist/providers/profiles.d.ts +5 -3
- package/dist/providers/profiles.js +3 -3
- package/dist/runtime/context-policy.d.ts +35 -0
- package/dist/runtime/context-policy.js +66 -0
- package/dist/runtime/eval.d.ts +6 -2
- package/dist/runtime/eval.js +2 -2
- package/dist/runtime/event-stream.d.ts +9 -0
- package/dist/runtime/event-stream.js +25 -11
- package/dist/runtime/execution-plane.d.ts +5 -1
- package/dist/runtime/execution-plane.js +9 -3
- package/dist/runtime/facade.js +2 -1
- package/dist/runtime/kernel-event-log.d.ts +0 -2
- package/dist/runtime/kernel-event-log.js +61 -13
- package/dist/runtime/kernel-primitives-dashboard.js +1 -1
- package/dist/runtime/kernel-rebuild.d.ts +13 -0
- package/dist/runtime/kernel-rebuild.js +75 -0
- package/dist/runtime/kernel-step.d.ts +157 -8
- package/dist/runtime/kernel-step.js +220 -7
- package/dist/runtime/kernel-transaction-log.d.ts +61 -0
- package/dist/runtime/kernel-transaction-log.js +149 -0
- package/dist/runtime/large-result-spool.d.ts +3 -1
- package/dist/runtime/large-result-spool.js +24 -5
- package/dist/runtime/loop-driver.d.ts +1 -1
- package/dist/runtime/loop-driver.js +2 -7
- package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
- package/dist/runtime/mcp-proxy-plane.js +23 -6
- 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 +33 -3
- package/dist/runtime/process-sandbox-plane.js +16 -11
- package/dist/runtime/reaction-checkpoint.d.ts +51 -0
- package/dist/runtime/reaction-checkpoint.js +83 -0
- package/dist/runtime/reactive-session.d.ts +9 -3
- package/dist/runtime/reactive-session.js +44 -14
- package/dist/runtime/reliability.d.ts +48 -0
- package/dist/runtime/reliability.js +86 -0
- package/dist/runtime/remote-vpc-plane.js +4 -3
- package/dist/runtime/run-group.d.ts +38 -35
- package/dist/runtime/run-group.js +97 -54
- package/dist/runtime/runner.d.ts +117 -78
- package/dist/runtime/runner.js +1516 -1197
- package/dist/runtime/session-log.d.ts +71 -12
- package/dist/runtime/session-log.js +192 -32
- package/dist/runtime/session-repair.d.ts +11 -7
- package/dist/runtime/session-repair.js +11 -8
- package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
- package/dist/runtime/sub-agent-orchestrator.js +45 -18
- package/dist/signals/gateway.d.ts +23 -10
- package/dist/signals/gateway.js +81 -17
- package/dist/signals/scheduled.js +0 -1
- package/dist/signals/types.d.ts +21 -13
- package/dist/skills/loader.js +12 -2
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/agent.d.ts +40 -1
- package/dist/types/agent.js +61 -1
- package/dist/types.d.ts +6 -1
- package/dist/workflow/public.d.ts +2 -3
- package/dist/workflow/public.js +0 -1
- package/package.json +2 -2
package/dist/memory/agent.d.ts
CHANGED
|
@@ -1,66 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Long-term memory agent implementation (Phase 7).
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* 1. Extract memories from conversation transcripts (后台代理)
|
|
6
|
-
* 2. Select relevant memories for current context (LLM选择器)
|
|
4
|
+
* Provides deterministic selection and boundary validation for long-term memories.
|
|
7
5
|
*
|
|
8
6
|
* Design principles:
|
|
9
7
|
* - Kernel defines memory types and validation rules
|
|
10
8
|
* - SDK performs I/O and selection
|
|
11
|
-
* -
|
|
9
|
+
* - Semantic reranking remains an optional DreamStore capability
|
|
12
10
|
*/
|
|
13
|
-
import type {
|
|
14
|
-
export type { MemoryKind,
|
|
15
|
-
/**
|
|
16
|
-
* Memory index entry (from MEMORY.md).
|
|
17
|
-
*/
|
|
18
|
-
export interface MemoryIndexEntry {
|
|
19
|
-
name: string;
|
|
20
|
-
description: string;
|
|
21
|
-
kind?: MemoryKind;
|
|
22
|
-
file: string;
|
|
23
|
-
updated_at: number;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Extract memories from a completed session.
|
|
27
|
-
*
|
|
28
|
-
* This is typically called by a background agent after a session completes.
|
|
29
|
-
* It analyzes the conversation and generates new memory write requests.
|
|
30
|
-
*
|
|
31
|
-
* @param sessionData - The completed session data
|
|
32
|
-
* @param existingMemories - Existing memories to avoid duplicates
|
|
33
|
-
* @returns Array of memory write requests
|
|
34
|
-
*/
|
|
35
|
-
export declare function extractMemories(sessionData: SessionData, existingMemories: MemoryEntry[]): Promise<MemoryWriteRequest[]>;
|
|
11
|
+
import type { MemoryQuery, MemoryRecall, MemoryRecord } from "./protocols.js";
|
|
12
|
+
export type { MemoryKind, MemoryQuery, MemoryRecall, MemoryRecord, } from "./protocols.js";
|
|
36
13
|
/**
|
|
37
14
|
* Select relevant memories for the current context.
|
|
38
15
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 1. Read memory index (name + description for each memory)
|
|
42
|
-
* 2. Filter out already_surfaced and recentTools
|
|
43
|
-
* 3. Send the filtered list to LLM with current context
|
|
44
|
-
* 4. LLM returns top-5 most relevant memory IDs
|
|
16
|
+
* The reference selector is deterministic and provider-independent: lexical overlap first,
|
|
17
|
+
* recency second. Semantic/embedding reranking belongs in a DreamStore plugin.
|
|
45
18
|
*
|
|
46
19
|
* @param query - Memory query from kernel
|
|
47
20
|
* @param memoryIndex - Memory index entries
|
|
48
21
|
* @param model - Optional model name (default: claude-sonnet-4-20250514)
|
|
49
22
|
* @returns Memory retrieval result
|
|
50
23
|
*/
|
|
51
|
-
export declare function selectMemories(query: MemoryQuery,
|
|
24
|
+
export declare function selectMemories(query: MemoryQuery, records: MemoryRecord[]): Promise<MemoryRecall[]>;
|
|
52
25
|
/**
|
|
53
26
|
* Validate memory before writing (kernel-side validation mirror).
|
|
54
27
|
*
|
|
55
28
|
* This SDK-side validation provides early feedback before sending to kernel.
|
|
56
29
|
*/
|
|
57
|
-
export declare function validateMemory(
|
|
30
|
+
export declare function validateMemory(record: MemoryRecord): {
|
|
58
31
|
valid: boolean;
|
|
59
32
|
error?: string;
|
|
60
33
|
};
|
|
61
|
-
/**
|
|
62
|
-
* Infer memory kind from metadata (mirrors kernel logic).
|
|
63
|
-
*/
|
|
64
|
-
export declare function inferMemoryKind(metadata: MemoryMetadata): MemoryKind;
|
|
65
|
-
/** Build a memory index from DreamStore entries for {@link selectMemories}. */
|
|
66
|
-
export declare function memoriesToIndex(entries: MemoryEntry[]): MemoryIndexEntry[];
|
package/dist/memory/agent.js
CHANGED
|
@@ -1,151 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Long-term memory agent implementation (Phase 7).
|
|
3
3
|
*
|
|
4
|
-
* Provides
|
|
5
|
-
* 1. Extract memories from conversation transcripts (后台代理)
|
|
6
|
-
* 2. Select relevant memories for current context (LLM选择器)
|
|
4
|
+
* Provides deterministic selection and boundary validation for long-term memories.
|
|
7
5
|
*
|
|
8
6
|
* Design principles:
|
|
9
7
|
* - Kernel defines memory types and validation rules
|
|
10
8
|
* - SDK performs I/O and selection
|
|
11
|
-
* -
|
|
9
|
+
* - Semantic reranking remains an optional DreamStore capability
|
|
12
10
|
*/
|
|
13
|
-
|
|
14
|
-
* Extract memories from a completed session.
|
|
15
|
-
*
|
|
16
|
-
* This is typically called by a background agent after a session completes.
|
|
17
|
-
* It analyzes the conversation and generates new memory write requests.
|
|
18
|
-
*
|
|
19
|
-
* @param sessionData - The completed session data
|
|
20
|
-
* @param existingMemories - Existing memories to avoid duplicates
|
|
21
|
-
* @returns Array of memory write requests
|
|
22
|
-
*/
|
|
23
|
-
export async function extractMemories(sessionData, existingMemories) {
|
|
24
|
-
// In a real implementation, this would:
|
|
25
|
-
// 1. Use an LLM to analyze the session transcript
|
|
26
|
-
// 2. Identify new insights, preferences, and context
|
|
27
|
-
// 3. Generate memory write requests with proper classification
|
|
28
|
-
// 4. Check for duplicates against existing memories
|
|
29
|
-
// For now, return a stub implementation
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
11
|
+
import { rankMemories } from "./ranking.js";
|
|
32
12
|
/**
|
|
33
13
|
* Select relevant memories for the current context.
|
|
34
14
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* 1. Read memory index (name + description for each memory)
|
|
38
|
-
* 2. Filter out already_surfaced and recentTools
|
|
39
|
-
* 3. Send the filtered list to LLM with current context
|
|
40
|
-
* 4. LLM returns top-5 most relevant memory IDs
|
|
15
|
+
* The reference selector is deterministic and provider-independent: lexical overlap first,
|
|
16
|
+
* recency second. Semantic/embedding reranking belongs in a DreamStore plugin.
|
|
41
17
|
*
|
|
42
18
|
* @param query - Memory query from kernel
|
|
43
19
|
* @param memoryIndex - Memory index entries
|
|
44
20
|
* @param model - Optional model name (default: claude-sonnet-4-20250514)
|
|
45
21
|
* @returns Memory retrieval result
|
|
46
22
|
*/
|
|
47
|
-
export async function selectMemories(query,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// - Call the LLM API
|
|
64
|
-
// - Parse the response to extract selected memory IDs
|
|
65
|
-
// For now, return a stub implementation
|
|
66
|
-
return {
|
|
67
|
-
selected_memory_ids: candidates.slice(0, query.top_k).map((c) => c.name),
|
|
68
|
-
selection_rationale: "Stub implementation",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Check if a memory is tool-related (should be filtered from recentTools).
|
|
73
|
-
*/
|
|
74
|
-
function isToolMemory(entry) {
|
|
75
|
-
const toolKeywords = ["usage", "how to use", "example", "syntax", "api"];
|
|
76
|
-
const lowerDesc = entry.description.toLowerCase();
|
|
77
|
-
const lowerName = entry.name.toLowerCase();
|
|
78
|
-
// Filter out usage docs, but keep warnings/caveats
|
|
79
|
-
const isUsage = toolKeywords.some((kw) => lowerDesc.includes(kw) || lowerName.includes(kw));
|
|
80
|
-
const isWarning = lowerDesc.includes("warning") || lowerDesc.includes("caveat") || lowerDesc.includes("bug");
|
|
81
|
-
return isUsage && !isWarning;
|
|
23
|
+
export async function selectMemories(query, records) {
|
|
24
|
+
const candidates = records.filter(record => record.scope.tenant_id === query.scope.tenant_id
|
|
25
|
+
&& record.scope.namespace === query.scope.namespace
|
|
26
|
+
&& (query.kinds.length === 0 || query.kinds.includes(record.kind)));
|
|
27
|
+
const ranked = rankMemories(query.query, candidates.map((record, insertionIndex) => ({
|
|
28
|
+
value: record,
|
|
29
|
+
searchableText: `${record.name} ${record.description} ${record.content}`,
|
|
30
|
+
updatedAt: Number.isFinite(record.updated_at) ? record.updated_at : 0,
|
|
31
|
+
recallCount: record.recall_count,
|
|
32
|
+
ttlDays: record.ttl_days,
|
|
33
|
+
insertionIndex,
|
|
34
|
+
})), query.top_k);
|
|
35
|
+
// score is relevance (from ranking), deliberately distinct from the record's stored confidence.
|
|
36
|
+
return ranked
|
|
37
|
+
.filter(hit => query.min_score === undefined || hit.score >= query.min_score)
|
|
38
|
+
.map(hit => ({ record: hit.value, score: hit.score, why: hit.why }));
|
|
82
39
|
}
|
|
83
40
|
/**
|
|
84
41
|
* Validate memory before writing (kernel-side validation mirror).
|
|
85
42
|
*
|
|
86
43
|
* This SDK-side validation provides early feedback before sending to kernel.
|
|
87
44
|
*/
|
|
88
|
-
export function validateMemory(
|
|
45
|
+
export function validateMemory(record) {
|
|
89
46
|
// Check required fields
|
|
90
|
-
if (!
|
|
47
|
+
if (!record.record_id || record.record_id.trim().length === 0) {
|
|
48
|
+
return { valid: false, error: "Missing required field: record_id" };
|
|
49
|
+
}
|
|
50
|
+
if (!record.scope.tenant_id || !record.scope.namespace) {
|
|
51
|
+
return { valid: false, error: "Missing required field: scope" };
|
|
52
|
+
}
|
|
53
|
+
if (!record.name || record.name.trim().length === 0) {
|
|
91
54
|
return { valid: false, error: "Missing required field: name" };
|
|
92
55
|
}
|
|
93
|
-
if (!
|
|
56
|
+
if (!record.description || record.description.trim().length === 0) {
|
|
94
57
|
return { valid: false, error: "Missing required field: description" };
|
|
95
58
|
}
|
|
96
59
|
// Check name length
|
|
97
|
-
if (
|
|
98
|
-
return { valid: false, error: `Name too long: ${
|
|
60
|
+
if (record.name.length > 100) {
|
|
61
|
+
return { valid: false, error: `Name too long: ${record.name.length} chars (limit: 100)` };
|
|
99
62
|
}
|
|
100
63
|
// Check content size
|
|
101
|
-
if (
|
|
102
|
-
return { valid: false, error: `Content too large: ${
|
|
103
|
-
}
|
|
104
|
-
// Check forbidden patterns
|
|
105
|
-
const forbiddenPatterns = [
|
|
106
|
-
{ pattern: "代码模式:", reason: "应从代码推,不应存储" },
|
|
107
|
-
{ pattern: "文件路径:", reason: "应从git推,不应存储" },
|
|
108
|
-
{ pattern: "架构:", reason: "应从实际代码推" },
|
|
109
|
-
{ pattern: "git历史:", reason: "git log是权威" },
|
|
110
|
-
{ pattern: "CLAUDE.md:", reason: "已在文档中" },
|
|
111
|
-
{ pattern: "TODO:", reason: "临时任务不应进记忆" },
|
|
112
|
-
];
|
|
113
|
-
for (const { pattern, reason } of forbiddenPatterns) {
|
|
114
|
-
if (request.content.includes(pattern)) {
|
|
115
|
-
return { valid: false, error: `Forbidden pattern '${pattern}': ${reason}` };
|
|
116
|
-
}
|
|
64
|
+
if (record.content.length > 10_000) {
|
|
65
|
+
return { valid: false, error: `Content too large: ${record.content.length} bytes (limit: 10000)` };
|
|
117
66
|
}
|
|
118
67
|
return { valid: true };
|
|
119
68
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Infer memory kind from metadata (mirrors kernel logic).
|
|
122
|
-
*/
|
|
123
|
-
export function inferMemoryKind(metadata) {
|
|
124
|
-
if (metadata.user_role || metadata.expertise_level) {
|
|
125
|
-
return "user";
|
|
126
|
-
}
|
|
127
|
-
if (metadata.preference_rule || metadata.approved_pattern) {
|
|
128
|
-
return "feedback";
|
|
129
|
-
}
|
|
130
|
-
if (metadata.project_phase || metadata.relative_date) {
|
|
131
|
-
return "project";
|
|
132
|
-
}
|
|
133
|
-
if (metadata.external_url || metadata.ticket_ref) {
|
|
134
|
-
return "reference";
|
|
135
|
-
}
|
|
136
|
-
// Default: feedback (most common)
|
|
137
|
-
return "feedback";
|
|
138
|
-
}
|
|
139
|
-
/** Build a memory index from DreamStore entries for {@link selectMemories}. */
|
|
140
|
-
export function memoriesToIndex(entries) {
|
|
141
|
-
return entries.map(entry => {
|
|
142
|
-
const meta = (entry.metadata ?? {});
|
|
143
|
-
return {
|
|
144
|
-
name: String(meta.name ?? entry.text.slice(0, 40)),
|
|
145
|
-
description: String(meta.description ?? entry.text.slice(0, 120)),
|
|
146
|
-
kind: meta.kind,
|
|
147
|
-
file: String(meta.file ?? ""),
|
|
148
|
-
updated_at: Number(meta.updated_at ?? 0),
|
|
149
|
-
};
|
|
150
|
-
});
|
|
151
|
-
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LLMProvider } from "../types.js";
|
|
2
|
+
import type { MemoryRecord, MemoryScope, SessionData } from "./protocols.js";
|
|
3
|
+
export declare function extractSessionMemories(provider: LLMProvider, session: SessionData, scope: MemoryScope, systemPrompt?: string): Promise<MemoryRecord[]>;
|
|
4
|
+
export declare function parseExtractedMemories(output: string, session: SessionData, scope: MemoryScope): MemoryRecord[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const KINDS = new Set(["user", "feedback", "project", "reference"]);
|
|
2
|
+
export async function extractSessionMemories(provider, session, scope, systemPrompt) {
|
|
3
|
+
const transcript = session.messages
|
|
4
|
+
.map(message => `[${message.role.toUpperCase()}] ${message.content}`)
|
|
5
|
+
.join("\n")
|
|
6
|
+
.slice(0, 8_000);
|
|
7
|
+
const context = {
|
|
8
|
+
systemText: [
|
|
9
|
+
systemPrompt,
|
|
10
|
+
"Extract durable, reusable facts from this completed session. Return only JSON; do not include transient progress or guesses.",
|
|
11
|
+
].filter(Boolean).join("\n\n"),
|
|
12
|
+
turns: [{
|
|
13
|
+
role: "user",
|
|
14
|
+
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.`,
|
|
15
|
+
toolCalls: [],
|
|
16
|
+
}],
|
|
17
|
+
};
|
|
18
|
+
let output = "";
|
|
19
|
+
const state = provider.createRunState?.();
|
|
20
|
+
for await (const event of provider.stream(context, [], undefined, state)) {
|
|
21
|
+
if (event.type === "text_delta" && "delta" in event)
|
|
22
|
+
output += event.delta;
|
|
23
|
+
}
|
|
24
|
+
return parseExtractedMemories(output, session, scope);
|
|
25
|
+
}
|
|
26
|
+
export function parseExtractedMemories(output, session, scope) {
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(output.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.memories))
|
|
35
|
+
return [];
|
|
36
|
+
const now = session.updatedAtMs;
|
|
37
|
+
const records = [];
|
|
38
|
+
for (const raw of value.memories.slice(0, 10)) {
|
|
39
|
+
if (!raw || typeof raw !== "object")
|
|
40
|
+
continue;
|
|
41
|
+
const draft = raw;
|
|
42
|
+
const name = typeof draft.name === "string" ? draft.name.trim() : "";
|
|
43
|
+
const kind = typeof draft.kind === "string" && KINDS.has(draft.kind)
|
|
44
|
+
? draft.kind
|
|
45
|
+
: undefined;
|
|
46
|
+
const content = typeof draft.content === "string" ? draft.content.trim() : "";
|
|
47
|
+
if (!name || !kind || !content)
|
|
48
|
+
continue;
|
|
49
|
+
const confidence = typeof draft.confidence === "number" && Number.isFinite(draft.confidence)
|
|
50
|
+
? Math.max(0, Math.min(1, draft.confidence))
|
|
51
|
+
: 0.5;
|
|
52
|
+
records.push({
|
|
53
|
+
record_id: `${scope.tenant_id}:${scope.namespace}:${kind}:${name}`,
|
|
54
|
+
scope,
|
|
55
|
+
name,
|
|
56
|
+
kind,
|
|
57
|
+
content,
|
|
58
|
+
description: typeof draft.description === "string" ? draft.description.trim() : "",
|
|
59
|
+
provenance: {
|
|
60
|
+
session_id: session.sessionId,
|
|
61
|
+
author: "extraction",
|
|
62
|
+
trust: "untrusted",
|
|
63
|
+
evidence_refs: Array.isArray(draft.evidence_refs)
|
|
64
|
+
? draft.evidence_refs.filter((ref) => typeof ref === "string")
|
|
65
|
+
: [],
|
|
66
|
+
},
|
|
67
|
+
created_at: now,
|
|
68
|
+
updated_at: now,
|
|
69
|
+
recall_count: 0,
|
|
70
|
+
confidence,
|
|
71
|
+
links: Array.isArray(draft.links) ? draft.links.filter((link) => typeof link === "string") : [],
|
|
72
|
+
pinned: draft.pinned === true,
|
|
73
|
+
...(typeof draft.ttl_days === "number" && Number.isInteger(draft.ttl_days) && draft.ttl_days > 0
|
|
74
|
+
? { ttl_days: draft.ttl_days }
|
|
75
|
+
: {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return records;
|
|
79
|
+
}
|
|
@@ -6,32 +6,45 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Use cases:
|
|
8
8
|
* - Benchmark A/B variants where memory is on/off (preload via constructor).
|
|
9
|
-
* - Unit tests that exercise
|
|
9
|
+
* - Unit tests that exercise session extraction or the `memory_query` path without disk I/O.
|
|
10
10
|
* - Local development / CI where a persistent memory store isn't needed.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Search returns a genuine relevance score (distinct lexical overlap, lifted by recall history and
|
|
13
|
+
* lowered by TTL/staleness) — never the record's stored confidence. The store is the authority for
|
|
14
|
+
* the full record set, so it (M3) bounds itself by value-ordered retention eviction and (M3/M4)
|
|
15
|
+
* mirrors recall lifecycle and pin state. For semantic search, plug in a real store.
|
|
15
16
|
*/
|
|
16
|
-
import type {
|
|
17
|
+
import type { DreamStore, MemoryQuery, MemoryRecall, MemoryRecallLifecycle, MemoryRecord, SessionData } from "./protocols.js";
|
|
18
|
+
export interface InMemoryDreamStoreOptions {
|
|
19
|
+
/** Cap the per-agent record set; a write past it evicts the lowest-value unpinned records (M3). */
|
|
20
|
+
maxRecords?: number;
|
|
21
|
+
/** Age (days) past which a record's recall relevance is discounted. Default 2. */
|
|
22
|
+
staleWarningDays?: number;
|
|
23
|
+
/** Wall-clock source for staleness scoring + recall stamps. Injectable for deterministic tests. */
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
17
26
|
export declare class InMemoryDreamStore implements DreamStore {
|
|
18
27
|
private readonly initialMemories;
|
|
19
|
-
private sessions;
|
|
20
28
|
private memories;
|
|
21
29
|
/** Sessions persisted via `saveSession`; exposed for test assertions. */
|
|
22
30
|
readonly savedSessions: SessionData[];
|
|
31
|
+
private readonly maxRecords?;
|
|
32
|
+
private readonly staleWarningDays;
|
|
33
|
+
private readonly now;
|
|
23
34
|
/**
|
|
24
35
|
* @param initialMemories Optional seed memories applied to every agent that asks for memories
|
|
25
36
|
* for the first time. Useful for benchmark scenarios that preload a fact.
|
|
26
37
|
*/
|
|
27
|
-
constructor(initialMemories?:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
commit(agentId: string, result: CurationResult, existing: MemoryEntry[]): Promise<void>;
|
|
35
|
-
search(agentId: string, _query: string, topK?: number): Promise<MemoryEntry[]>;
|
|
38
|
+
constructor(initialMemories?: MemoryRecord[], options?: InMemoryDreamStoreOptions);
|
|
39
|
+
private recordsFor;
|
|
40
|
+
upsert(agentId: string, incoming: MemoryRecord): Promise<void>;
|
|
41
|
+
/** M3: value-ordered retention eviction. Sheds the lowest-value unpinned records — the shared
|
|
42
|
+
* deterministic formula, never a blind tail-cut — until the set fits `maxRecords`. */
|
|
43
|
+
private evictToCapacity;
|
|
44
|
+
search(agentId: string, query: MemoryQuery): Promise<MemoryRecall[]>;
|
|
36
45
|
saveSession(data: SessionData): Promise<void>;
|
|
46
|
+
/** M3: mirror the kernel's journaled recall lifecycle into the durable records. */
|
|
47
|
+
recordRecall(agentId: string, recalls: MemoryRecallLifecycle[]): Promise<void>;
|
|
48
|
+
/** M4: set a record's pin so retention eviction cannot shed it. */
|
|
49
|
+
setPinned(agentId: string, recordId: string, pinned: boolean): Promise<void>;
|
|
37
50
|
}
|
|
@@ -1,30 +1,24 @@
|
|
|
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
|
/** Sessions persisted via `saveSession`; exposed for test assertions. */
|
|
6
7
|
savedSessions = [];
|
|
8
|
+
maxRecords;
|
|
9
|
+
staleWarningDays;
|
|
10
|
+
now;
|
|
7
11
|
/**
|
|
8
12
|
* @param initialMemories Optional seed memories applied to every agent that asks for memories
|
|
9
13
|
* for the first time. Useful for benchmark scenarios that preload a fact.
|
|
10
14
|
*/
|
|
11
|
-
constructor(initialMemories = []) {
|
|
15
|
+
constructor(initialMemories = [], options = {}) {
|
|
12
16
|
this.initialMemories = initialMemories;
|
|
17
|
+
this.maxRecords = options.maxRecords;
|
|
18
|
+
this.staleWarningDays = options.staleWarningDays ?? 2;
|
|
19
|
+
this.now = options.now ?? Date.now;
|
|
13
20
|
}
|
|
14
|
-
|
|
15
|
-
addSession(agentId, session) {
|
|
16
|
-
const list = this.sessions.get(agentId) ?? [];
|
|
17
|
-
list.push(session);
|
|
18
|
-
this.sessions.set(agentId, list);
|
|
19
|
-
}
|
|
20
|
-
/** Pre-populate memories for a specific agent (test/benchmark setup). */
|
|
21
|
-
addMemories(agentId, entries) {
|
|
22
|
-
this.memories.set(agentId, [...(this.memories.get(agentId) ?? []), ...entries]);
|
|
23
|
-
}
|
|
24
|
-
async loadSessions(agentId) {
|
|
25
|
-
return this.sessions.get(agentId) ?? [];
|
|
26
|
-
}
|
|
27
|
-
async loadMemories(agentId) {
|
|
21
|
+
recordsFor(agentId) {
|
|
28
22
|
if (this.memories.has(agentId))
|
|
29
23
|
return this.memories.get(agentId);
|
|
30
24
|
if (this.initialMemories.length > 0) {
|
|
@@ -33,18 +27,73 @@ export class InMemoryDreamStore {
|
|
|
33
27
|
}
|
|
34
28
|
return [];
|
|
35
29
|
}
|
|
36
|
-
async
|
|
37
|
-
const kept =
|
|
38
|
-
|
|
30
|
+
async upsert(agentId, incoming) {
|
|
31
|
+
const kept = [...this.recordsFor(agentId)];
|
|
32
|
+
const index = kept.findIndex(record => record.scope.tenant_id === incoming.scope.tenant_id
|
|
33
|
+
&& record.scope.namespace === incoming.scope.namespace
|
|
34
|
+
&& record.kind === incoming.kind
|
|
35
|
+
&& record.name === incoming.name);
|
|
36
|
+
if (index >= 0)
|
|
37
|
+
kept[index] = incoming;
|
|
38
|
+
else
|
|
39
|
+
kept.push(incoming);
|
|
40
|
+
this.memories.set(agentId, this.evictToCapacity(kept));
|
|
41
|
+
}
|
|
42
|
+
/** M3: value-ordered retention eviction. Sheds the lowest-value unpinned records — the shared
|
|
43
|
+
* deterministic formula, never a blind tail-cut — until the set fits `maxRecords`. */
|
|
44
|
+
evictToCapacity(records) {
|
|
45
|
+
if (this.maxRecords === undefined || records.length <= this.maxRecords)
|
|
46
|
+
return records;
|
|
47
|
+
const nowMs = this.now();
|
|
48
|
+
const scored = records.map((record, insertionIndex) => ({
|
|
49
|
+
record,
|
|
50
|
+
insertionIndex,
|
|
51
|
+
score: record.pinned ? Number.POSITIVE_INFINITY : memoryRetentionScore(record, nowMs, this.staleWarningDays),
|
|
52
|
+
}));
|
|
53
|
+
// Keep the highest-value maxRecords; ties break on insertion order (older first survives).
|
|
54
|
+
scored.sort((a, b) => b.score - a.score || a.insertionIndex - b.insertionIndex);
|
|
55
|
+
return scored.slice(0, this.maxRecords).map(entry => entry.record);
|
|
39
56
|
}
|
|
40
|
-
async search(agentId,
|
|
41
|
-
const all =
|
|
42
|
-
|
|
57
|
+
async search(agentId, query) {
|
|
58
|
+
const all = this.recordsFor(agentId);
|
|
59
|
+
const candidates = all.filter(record => record.scope.tenant_id === query.scope.tenant_id
|
|
60
|
+
&& record.scope.namespace === query.scope.namespace
|
|
61
|
+
&& (query.kinds.length === 0 || query.kinds.includes(record.kind)));
|
|
62
|
+
const ranked = rankMemories(query.query, candidates.map((record, insertionIndex) => {
|
|
63
|
+
return {
|
|
64
|
+
value: record,
|
|
65
|
+
searchableText: `${record.name} ${record.description} ${record.content}`,
|
|
66
|
+
updatedAt: Number.isFinite(record.updated_at) ? record.updated_at : 0,
|
|
67
|
+
recallCount: record.recall_count,
|
|
68
|
+
ttlDays: record.ttl_days,
|
|
69
|
+
insertionIndex,
|
|
70
|
+
};
|
|
71
|
+
}), query.top_k, { nowMs: this.now(), staleWarningDays: this.staleWarningDays });
|
|
72
|
+
return ranked
|
|
73
|
+
.filter(hit => query.min_score === undefined || hit.score >= query.min_score)
|
|
74
|
+
.map(hit => ({ record: hit.value, score: hit.score, why: hit.why }));
|
|
43
75
|
}
|
|
44
76
|
async saveSession(data) {
|
|
45
77
|
this.savedSessions.push(data);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
}
|
|
79
|
+
/** M3: mirror the kernel's journaled recall lifecycle into the durable records. */
|
|
80
|
+
async recordRecall(agentId, recalls) {
|
|
81
|
+
const records = this.recordsFor(agentId);
|
|
82
|
+
for (const recall of recalls) {
|
|
83
|
+
const record = records.find(candidate => candidate.record_id === recall.record_id);
|
|
84
|
+
if (record) {
|
|
85
|
+
record.recall_count = recall.recall_count;
|
|
86
|
+
record.last_recalled_at = recall.last_recalled_at;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.memories.set(agentId, records);
|
|
90
|
+
}
|
|
91
|
+
/** M4: set a record's pin so retention eviction cannot shed it. */
|
|
92
|
+
async setPinned(agentId, recordId, pinned) {
|
|
93
|
+
const records = this.recordsFor(agentId);
|
|
94
|
+
const record = records.find(candidate => candidate.record_id === recordId);
|
|
95
|
+
if (record)
|
|
96
|
+
record.pinned = pinned;
|
|
97
|
+
this.memories.set(agentId, records);
|
|
49
98
|
}
|
|
50
99
|
}
|