cogmemory 0.0.1-beta.2
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 +139 -0
- package/dist/arbiter.cjs +22 -0
- package/dist/arbiter.cjs.map +1 -0
- package/dist/arbiter.d.ts +72 -0
- package/dist/arbiter.js +85 -0
- package/dist/arbiter.js.map +1 -0
- package/dist/cognitive-memory.cjs +15 -0
- package/dist/cognitive-memory.cjs.map +1 -0
- package/dist/cognitive-memory.js +1068 -0
- package/dist/cognitive-memory.js.map +1 -0
- package/dist/index.d.ts +1086 -0
- package/package.json +83 -0
- package/src/arbiter.ts +14 -0
- package/src/client.ts +105 -0
- package/src/cogmem.ts +74 -0
- package/src/cognitive/arbiter.ts +105 -0
- package/src/cognitive/fast-gate.ts +63 -0
- package/src/cognitive/index.ts +64 -0
- package/src/cognitive/memory.ts +878 -0
- package/src/cognitive/relevance.ts +199 -0
- package/src/cognitive/rules.ts +127 -0
- package/src/cognitive/types.ts +232 -0
- package/src/errors.ts +85 -0
- package/src/helpers/turn.ts +172 -0
- package/src/index.ts +134 -0
- package/src/resources/context.ts +17 -0
- package/src/resources/memories.ts +60 -0
- package/src/resources/recall.ts +17 -0
- package/src/resources/self-model.ts +21 -0
- package/src/resources/stats.ts +27 -0
- package/src/resources/tensions.ts +36 -0
- package/src/resources/turns.ts +16 -0
- package/src/types.ts +246 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic text, relevance and reconciliation primitives.
|
|
3
|
+
*
|
|
4
|
+
* The whole decision layer of a memory system, with no model, no database and
|
|
5
|
+
* no dependencies — so the service, an in-process runtime, and a test all make
|
|
6
|
+
* the same calls. The reasoning behind each threshold is written down because
|
|
7
|
+
* the numbers are only defensible with it:
|
|
8
|
+
*
|
|
9
|
+
* - A memory cannot be found by a model being smart, only by words overlapping.
|
|
10
|
+
* - Lexical overlap peaks on identical strings and bottoms out on the
|
|
11
|
+
* paraphrases that actually add information, so the *recall* floor is low and
|
|
12
|
+
* a separate step decides what to do with what came back.
|
|
13
|
+
* - Losing a fact is permanent; an extra duplicate costs one row. Every
|
|
14
|
+
* threshold below leans that way.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const STOP_WORDS = new Set([
|
|
18
|
+
"the", "and", "for", "this", "that", "with", "from", "you", "are", "was", "has", "have",
|
|
19
|
+
"what", "which", "when", "were", "will", "your", "our", "its", "not", "but", "all", "any",
|
|
20
|
+
"can", "did", "does", "how", "into", "out", "use", "used", "using", "one", "two", "get",
|
|
21
|
+
"new", "now", "then", "than", "them", "they", "his", "her", "she", "him", "been", "being",
|
|
22
|
+
"there", "here", "also", "just", "like", "make", "made", "need", "want", "about", "after",
|
|
23
|
+
"tell", "know", "give", "show", "please", "would", "could", "should", "shall"
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/** Words that appear in every restatement of a fact and so carry no identity. */
|
|
27
|
+
const FILLER_TOKENS = new Set([
|
|
28
|
+
"this", "that", "these", "those", "there", "here", "with", "from", "into", "must",
|
|
29
|
+
"should", "always", "never", "under", "over", "about", "after", "before", "when",
|
|
30
|
+
"where", "which", "what", "your", "their", "them", "they", "then", "than", "also",
|
|
31
|
+
"just", "only", "each", "every", "some", "such", "very", "more", "most", "same",
|
|
32
|
+
"file", "files", "name", "names", "project", "repository", "repo", "note"
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
/** Content words worth matching a memory against. */
|
|
36
|
+
export const relevanceTokens = (value: string): Set<string> =>
|
|
37
|
+
new Set(
|
|
38
|
+
value
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.split(/[^a-z0-9]+/)
|
|
41
|
+
.filter((word) => word.length >= 3 && !STOP_WORDS.has(word))
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
/** Overlap of two token sets, normalised by the smaller side. */
|
|
45
|
+
export const overlapScore = (query: Set<string>, candidate: Set<string>): number => {
|
|
46
|
+
if (query.size === 0 || candidate.size === 0) return 0
|
|
47
|
+
let shared = 0
|
|
48
|
+
for (const word of query) if (candidate.has(word)) shared += 1
|
|
49
|
+
return shared / Math.min(query.size, candidate.size)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Floor for *candidate* recall, not a similarity decision.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately low. This only decides who gets adjudicated; a tight gate misses
|
|
56
|
+
* exactly the paraphrases worth merging.
|
|
57
|
+
*/
|
|
58
|
+
export const CANDIDATE_FLOOR = 0.3
|
|
59
|
+
|
|
60
|
+
/** Above this, a memory is worth promoting into the pre-staged index. */
|
|
61
|
+
export const PROMOTE_THRESHOLD = 0.12
|
|
62
|
+
|
|
63
|
+
/** How many existing memories to put in front of an adjudicator. */
|
|
64
|
+
export const MAX_CANDIDATES = 8
|
|
65
|
+
|
|
66
|
+
/** Two results this similar are the same memory, restated. */
|
|
67
|
+
export const COLLAPSE_THRESHOLD = 0.8
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tokens that carry a fact's identity: identifiers, numbers, codes.
|
|
71
|
+
*
|
|
72
|
+
* Function words and generic nouns are dropped because they recur in every
|
|
73
|
+
* restatement and hide real differences.
|
|
74
|
+
*/
|
|
75
|
+
export const distinctiveTokens = (value: string): Set<string> =>
|
|
76
|
+
new Set(
|
|
77
|
+
value
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/['']/g, "")
|
|
80
|
+
.split(/[^a-z0-9]+/)
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.filter((w) => /\d/.test(w) || w.length >= 4)
|
|
83
|
+
.filter((w) => !FILLER_TOKENS.has(w))
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether rewriting `original` as `replacement` would drop information.
|
|
88
|
+
*
|
|
89
|
+
* Distinctive tokens carry a fact's identity, so a replacement missing one has
|
|
90
|
+
* deleted something — usually the qualifier that made the two statements differ
|
|
91
|
+
* at all ("never production", a build id, a port). Trailing plurals are folded
|
|
92
|
+
* so "deploys" merging into "deploy" is not read as a deletion.
|
|
93
|
+
*
|
|
94
|
+
* Biased towards reporting a loss: a false positive costs a duplicate entry,
|
|
95
|
+
* which is recoverable, while a false negative deletes a fact for good.
|
|
96
|
+
*/
|
|
97
|
+
export const isLossyRewrite = (original: string, replacement: string): boolean => {
|
|
98
|
+
const fold = (tokens: Set<string>): Set<string> =>
|
|
99
|
+
new Set([...tokens].map((t) => (t.length >= 4 && t.endsWith("s") ? t.slice(0, -1) : t)))
|
|
100
|
+
const after = fold(distinctiveTokens(replacement))
|
|
101
|
+
for (const token of fold(distinctiveTokens(original))) {
|
|
102
|
+
if (!after.has(token)) return true
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Dotted names that are not prose: hostnames, package names, file names.
|
|
109
|
+
*
|
|
110
|
+
* `internal-hbr-2291.pineapple.example` is precisely the kind of value a user
|
|
111
|
+
* names and a memory holds, and without this the identifier patterns below missed
|
|
112
|
+
* it — the trigger silently never fired for a host, which is one of the cases it
|
|
113
|
+
* exists for. The last label must be alphabetic, which is what keeps version
|
|
114
|
+
* strings and abbreviations out; a short stoplist covers the rest.
|
|
115
|
+
*/
|
|
116
|
+
const HOSTNAME = /\b[a-z0-9][a-z0-9-]{1,}(?:\.[a-z0-9-]+)*\.[a-z]{2,}\b/gi
|
|
117
|
+
|
|
118
|
+
/** Dotted tokens that are ordinary writing, not names. */
|
|
119
|
+
const NOT_A_HOSTNAME = new Set([
|
|
120
|
+
"eg", "ie", "etc", "vs", "approx", "inc", "ltd", "corp", "dept", "est", "fig", "no", "al"
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Identifiers worth matching a memory against: URLs, paths, hostnames,
|
|
125
|
+
* SCREAMING_SNAKE, camelCase, long kebab-case and hex-ish codes.
|
|
126
|
+
*
|
|
127
|
+
* The same idea as Aider's `mentioned_idents`: a user naming a concrete thing
|
|
128
|
+
* is a much stronger signal than the words around it. This is what turns a
|
|
129
|
+
* recall into a full-body injection without asking a model.
|
|
130
|
+
*/
|
|
131
|
+
export const extractIdentifiers = (text: string): string[] => {
|
|
132
|
+
const found = new Set<string>()
|
|
133
|
+
for (const match of text.match(/\bhttps?:\/\/[^\s<>()[\]"'`]+/g) ?? []) found.add(match)
|
|
134
|
+
for (const match of text.match(HOSTNAME) ?? []) {
|
|
135
|
+
if (NOT_A_HOSTNAME.has(match.toLowerCase())) continue
|
|
136
|
+
found.add(match)
|
|
137
|
+
}
|
|
138
|
+
for (const match of text.match(/\b(?:\.{0,2}\/)?[\w-]+(?:\/[\w.-]+)+\/?/g) ?? []) {
|
|
139
|
+
if (match.length > 3) found.add(match)
|
|
140
|
+
}
|
|
141
|
+
for (const match of text.match(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g) ?? []) found.add(match)
|
|
142
|
+
for (const match of text.match(/\b[A-Z]{2,}[0-9][A-Z0-9-]*\b/g) ?? []) found.add(match)
|
|
143
|
+
for (const match of text.match(/\b[a-z]+(?:[A-Z][a-z0-9]+){1,}\b/g) ?? []) found.add(match)
|
|
144
|
+
for (const match of text.match(/\b[a-z]+(?:-[a-z0-9]+){2,}\b/g) ?? []) found.add(match)
|
|
145
|
+
for (const match of text.match(/\b[0-9a-f]{6,}\b/gi) ?? []) found.add(match)
|
|
146
|
+
return [...found]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Instructions about *this conversation* rather than durable facts about the
|
|
151
|
+
* project.
|
|
152
|
+
*
|
|
153
|
+
* "Do not verify the staging build against the repository" and "just remember
|
|
154
|
+
* this" describe how to behave right now, so storing them produces noise that
|
|
155
|
+
* later looks like a project constraint.
|
|
156
|
+
*
|
|
157
|
+
* Explicit patterns rather than a "is this specific enough?" heuristic, because
|
|
158
|
+
* over-filtering would silently lose real memories, which is the worse failure.
|
|
159
|
+
*/
|
|
160
|
+
const INTERACTION_SCOPED = [
|
|
161
|
+
/\b(?:do not|don'?t|never|no need to)\s+(?:verify|check|confirm|look\s?up|search|investigate|browse|resolve)\b/i,
|
|
162
|
+
/\bjust\s+(?:remember|note|acknowledge|retain|treat)\b/i,
|
|
163
|
+
/\b(?:held|noted|stored|remembered)\s+(?:in|for)\s+(?:this|the)\s+conversation\b/i,
|
|
164
|
+
/\bnot\s+verified\b/i,
|
|
165
|
+
/\bwithout\s+verifying\b/i,
|
|
166
|
+
/\bfor\s+this\s+(?:conversation|session|turn|reply|response)\s+only\b/i,
|
|
167
|
+
/^(?:ok|okay|noted|got it|sure|thanks)\b[.!]?$/i
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
export const isInteractionScoped = (content: string): boolean =>
|
|
171
|
+
INTERACTION_SCOPED.some((pattern) => pattern.test(content))
|
|
172
|
+
|
|
173
|
+
/** Similarity of two statements by the tokens that carry identity. */
|
|
174
|
+
export const similarity = (a: string, b: string): number => {
|
|
175
|
+
const left = distinctiveTokens(a)
|
|
176
|
+
const right = distinctiveTokens(b)
|
|
177
|
+
if (left.size === 0 || right.size === 0) return 0
|
|
178
|
+
let shared = 0
|
|
179
|
+
for (const word of left) if (right.has(word)) shared += 1
|
|
180
|
+
return shared / Math.min(left.size, right.size)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const normalise = (value: string): string => value.trim().toLowerCase()
|
|
184
|
+
|
|
185
|
+
/** Approximate token cost of a string. */
|
|
186
|
+
export const estimateTokens = (value: string): number => Math.ceil(value.length / 4)
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The index line: short, scannable, no body.
|
|
190
|
+
*
|
|
191
|
+
* Falling back to the first sentence keeps an index line readable when a memory
|
|
192
|
+
* arrives from the API without a gist.
|
|
193
|
+
*/
|
|
194
|
+
export const gistOf = (item: { readonly content: string; readonly gist?: string | undefined }): string => {
|
|
195
|
+
if (item.gist && item.gist.trim().length > 0) return item.gist.trim()
|
|
196
|
+
const first = item.content.split(/(?<=[.!?])\s/)[0] ?? item.content
|
|
197
|
+
const trimmed = first.trim()
|
|
198
|
+
return trimmed.length > 90 ? `${trimmed.slice(0, 90)}…` : trimmed
|
|
199
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic fact extraction.
|
|
3
|
+
*
|
|
4
|
+
* A model extractor is good at judgement but it is still a model: it can
|
|
5
|
+
* refuse, hedge, or return nothing, and a fact the user plainly stated then
|
|
6
|
+
* never gets learned. Everything stated unambiguously is captured here with
|
|
7
|
+
* plain pattern matching, and the model is layered on top for the subtler cases.
|
|
8
|
+
*
|
|
9
|
+
* Only high-confidence shapes are matched. Being wrong in this direction writes
|
|
10
|
+
* a useless memory; being silent loses a real one, which is the worse failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const URL = /\bhttps?:\/\/[^\s<>()[\]"'`]+/g
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Positive requirements only.
|
|
17
|
+
*
|
|
18
|
+
* "never X" reads correctly as a requirement. "do not X" / "don't X" must NOT be
|
|
19
|
+
* captured here: naively taking the object of a negation produces nonsense like
|
|
20
|
+
* "The user requires: verify it against the repo" from "do not verify it against
|
|
21
|
+
* the repo", which inverts the meaning.
|
|
22
|
+
*/
|
|
23
|
+
const REQUIREMENT =
|
|
24
|
+
/\b(always|never|must|should|make sure to|remember to)\b[\s:]+([^.?!\n]{4,160})/gi
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* "<subject> is <value>".
|
|
28
|
+
*
|
|
29
|
+
* The value must NOT stop at the first `.`: that truncates
|
|
30
|
+
* `internal-hbr-2291.pineapple.example` to `internal-hbr-2291` and then poisons
|
|
31
|
+
* recall with the broken value. Stop at a period only when it ends a sentence.
|
|
32
|
+
*/
|
|
33
|
+
const ASSIGNMENT = /\b([A-Za-z][\w ()-]{2,60}?)\s+(?:is|are)\s+([^\n?!]{3,200})/g
|
|
34
|
+
|
|
35
|
+
const CODEISH = /[A-Z0-9_./:-]/
|
|
36
|
+
|
|
37
|
+
const looksLikeValue = (value: string): boolean => {
|
|
38
|
+
const trimmed = value.trim()
|
|
39
|
+
if (trimmed.length < 3 || trimmed.length > 200) return false
|
|
40
|
+
if (/^(not|nothing|unclear|unknown|the same|that|this|it)\b/i.test(trimmed)) return false
|
|
41
|
+
return CODEISH.test(trimmed)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Cut a captured clause back to its first complete sentence. */
|
|
45
|
+
const firstSentence = (value: string): string => {
|
|
46
|
+
const match = /\.(?=\s+[A-Z(])/.exec(value)
|
|
47
|
+
return match ? value.slice(0, match.index) : value
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tidy = (value: string): string =>
|
|
51
|
+
firstSentence(value)
|
|
52
|
+
.replace(/\s+/g, " ")
|
|
53
|
+
.replace(/^(?:that|the|a|an|it|this)\s+/i, "")
|
|
54
|
+
.replace(/[,;:\s.。]+$/, "")
|
|
55
|
+
.trim()
|
|
56
|
+
|
|
57
|
+
export interface DeterministicMemory {
|
|
58
|
+
readonly content: string
|
|
59
|
+
readonly domains: Array<string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract statements the user made that are worth remembering, without a model.
|
|
64
|
+
*/
|
|
65
|
+
export const extractDeterministic = (userMessage: string): Array<DeterministicMemory> => {
|
|
66
|
+
const out: Array<DeterministicMemory> = []
|
|
67
|
+
const push = (content: string): void => {
|
|
68
|
+
const cleaned = tidy(content)
|
|
69
|
+
if (cleaned.length < 8 || cleaned.length > 300) return
|
|
70
|
+
if (/^(yes|no|ok|okay|sure|thanks|thank you|got it|noted)\b/i.test(cleaned)) return
|
|
71
|
+
if (out.some((existing) => existing.content.toLowerCase() === cleaned.toLowerCase())) return
|
|
72
|
+
out.push({ content: cleaned, domains: [] })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 1. URLs are unambiguous.
|
|
76
|
+
for (const match of userMessage.match(URL) ?? []) {
|
|
77
|
+
push(`User-provided URL: ${match}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Stated requirements, kept in the user's own words.
|
|
81
|
+
// Paraphrasing flipped meaning once: "Never force push" became
|
|
82
|
+
// "The user requires: force push" — the exact opposite instruction.
|
|
83
|
+
for (const match of userMessage.matchAll(REQUIREMENT)) {
|
|
84
|
+
const verb = (match[1] ?? "").toLowerCase()
|
|
85
|
+
const body = tidy(match[2] ?? "")
|
|
86
|
+
if (!body) continue
|
|
87
|
+
push(`User requirement (${verb}): ${body}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. "X is Y" where Y is identifier-shaped.
|
|
91
|
+
for (const match of userMessage.matchAll(ASSIGNMENT)) {
|
|
92
|
+
const subject = tidy(match[1] ?? "")
|
|
93
|
+
const value = tidy(match[2] ?? "")
|
|
94
|
+
if (!subject || !looksLikeValue(value)) continue
|
|
95
|
+
push(`${subject} is ${value}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The three passes overlap: "Always run the canary pipeline for staging" gets
|
|
99
|
+
// caught as a requirement *and* as an assignment, and the assignment copy is
|
|
100
|
+
// truncated mid-value. Two rows for one sentence is noise the engine then has
|
|
101
|
+
// to reconcile later, so the contained one is dropped here instead.
|
|
102
|
+
const deduped = out.filter(
|
|
103
|
+
(candidate, index) =>
|
|
104
|
+
!out.some((other, otherIndex) => {
|
|
105
|
+
if (otherIndex === index) return false
|
|
106
|
+
if (other.content.length <= candidate.content.length) return false
|
|
107
|
+
return isCoveredBy(candidate.content, other.content)
|
|
108
|
+
})
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return deduped.slice(0, 6)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether everything worth keeping in `narrow` is already in `wide`.
|
|
116
|
+
*
|
|
117
|
+
* Compared on the tokens that carry a fact's identity, so dropping a number or a
|
|
118
|
+
* qualifier counts as loss even when the wording differs.
|
|
119
|
+
*/
|
|
120
|
+
const isCoveredBy = (narrow: string, wide: string): boolean => {
|
|
121
|
+
const have = new Set(wide.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length >= 4))
|
|
122
|
+
const covered = narrow
|
|
123
|
+
.toLowerCase()
|
|
124
|
+
.split(/[^a-z0-9]+/)
|
|
125
|
+
.filter((word) => word.length >= 4)
|
|
126
|
+
return covered.length > 0 && covered.every((word) => have.has(word))
|
|
127
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognitive Memory types and contracts for not-another-harness.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type MemoryTier = "L0" | "L1" | "L2" | "L3";
|
|
6
|
+
|
|
7
|
+
export type TensionStatus = "active" | "latent" | "resolved";
|
|
8
|
+
|
|
9
|
+
export type TensionImpact = "low" | "medium" | "critical";
|
|
10
|
+
|
|
11
|
+
export interface KnowledgeTension {
|
|
12
|
+
id: string;
|
|
13
|
+
status: TensionStatus;
|
|
14
|
+
claimA: {
|
|
15
|
+
source: string;
|
|
16
|
+
statement: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
};
|
|
19
|
+
claimB: {
|
|
20
|
+
source: string;
|
|
21
|
+
statement: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
};
|
|
24
|
+
impact: TensionImpact;
|
|
25
|
+
taskRelevance: number; // 0.0 - 1.0
|
|
26
|
+
actionableQuestion: string;
|
|
27
|
+
resolution?: {
|
|
28
|
+
resolvedAt: number;
|
|
29
|
+
resolvedBy: string;
|
|
30
|
+
pattern: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DomainCapability {
|
|
35
|
+
reliabilityScore: number; // 0.0 - 1.0 (historical success rate)
|
|
36
|
+
sampleCount: number;
|
|
37
|
+
knownFailurePatterns: string[];
|
|
38
|
+
recommendedStrategies: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProprioceptiveSelfModel {
|
|
42
|
+
/** Map of domain tag (e.g. "auth", "db-migration", "css") to capability stats */
|
|
43
|
+
domains: Record<string, DomainCapability>;
|
|
44
|
+
/** Overall calibration score (1.0 = well-calibrated, <0.7 = overconfident) */
|
|
45
|
+
calibrationFactor: number;
|
|
46
|
+
/** Active domains detected in current work */
|
|
47
|
+
activeDomains: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MemoryMetadata {
|
|
51
|
+
domains: string[];
|
|
52
|
+
isFailurePattern?: boolean;
|
|
53
|
+
isSuccessfulStrategy?: boolean;
|
|
54
|
+
isGuardrail?: boolean;
|
|
55
|
+
sourceSessionId?: string;
|
|
56
|
+
createdAt: number;
|
|
57
|
+
lastAccessedAt: number;
|
|
58
|
+
accessCount: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MemoryItem {
|
|
62
|
+
id: string;
|
|
63
|
+
content: string;
|
|
64
|
+
/** Dense 1-2 sentence representation for fast arbiter scanning */
|
|
65
|
+
bookmark: string;
|
|
66
|
+
/**
|
|
67
|
+
* Short label shown in the pre-staged index (Claude Code's `MEMORY.md` pattern:
|
|
68
|
+
* the index is always in context, the body is fetched on demand). Falls back to
|
|
69
|
+
* a truncation of `content` when the extractor did not supply one.
|
|
70
|
+
*/
|
|
71
|
+
gist?: string;
|
|
72
|
+
/** Dense embedding vector (empty array if embedding disabled) */
|
|
73
|
+
embedding?: number[];
|
|
74
|
+
tier: MemoryTier;
|
|
75
|
+
metadata: MemoryMetadata;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Why a memory ended up in the prompt — the thing you tune once you log it. */
|
|
79
|
+
/** What to do with a candidate memory once compared against what we hold. */
|
|
80
|
+
export type MemoryReconciliation = {
|
|
81
|
+
/**
|
|
82
|
+
* `add` keeps it as a new memory and is the safe default when unsure: an
|
|
83
|
+
* unmerged duplicate costs a row, while a wrong merge corrupts what we believe.
|
|
84
|
+
* `merge` collapses a restatement into the existing entry, keeping whichever
|
|
85
|
+
* carries more information. `replace` supersedes the existing entry. `reject`
|
|
86
|
+
* drops it as not worth storing.
|
|
87
|
+
*/
|
|
88
|
+
action: "add" | "merge" | "replace" | "reject";
|
|
89
|
+
/** Content to store when merging or replacing; defaults to the candidate. */
|
|
90
|
+
content?: string;
|
|
91
|
+
/** Why, so a bad merge is diagnosable rather than mysterious. */
|
|
92
|
+
reason?: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
export type MemoryInclusionReason =
|
|
97
|
+
/** Shown in the always-present index as a gist only. */
|
|
98
|
+
| "index"
|
|
99
|
+
/** Unseen identifier in the user's message matched this memory: body included. */
|
|
100
|
+
| "trigger"
|
|
101
|
+
/** Active knowledge tension: body included. */
|
|
102
|
+
| "tension"
|
|
103
|
+
/** Proprioceptive guardrail: body included. */
|
|
104
|
+
| "guardrail";
|
|
105
|
+
|
|
106
|
+
export interface MemoryInjectionEntry {
|
|
107
|
+
id: string;
|
|
108
|
+
tier: MemoryTier;
|
|
109
|
+
reason: MemoryInclusionReason;
|
|
110
|
+
/** Index line only. */
|
|
111
|
+
gist: string;
|
|
112
|
+
/** Present only when the body was worth the tokens. */
|
|
113
|
+
body?: string;
|
|
114
|
+
/** Rough token cost of what was included. */
|
|
115
|
+
tokens: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What was put in the prompt this turn, and why. */
|
|
119
|
+
export interface MemoryInjectionReport {
|
|
120
|
+
text: string;
|
|
121
|
+
entries: MemoryInjectionEntry[];
|
|
122
|
+
totalTokens: number;
|
|
123
|
+
/** True when the total budget forced something out. */
|
|
124
|
+
truncated: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface TrajectoryPrediction {
|
|
128
|
+
predictedDomains: string[];
|
|
129
|
+
predictedFiles: string[];
|
|
130
|
+
prefetchMemoryIds: string[];
|
|
131
|
+
confidence: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ArbiterEvaluationResult {
|
|
135
|
+
promotions: Array<{
|
|
136
|
+
memoryId: string;
|
|
137
|
+
targetTier: "L1";
|
|
138
|
+
signalType: "anticipatory" | "tension" | "proprioceptive" | "recency";
|
|
139
|
+
urgency: number; // 0.0 - 1.0
|
|
140
|
+
}>;
|
|
141
|
+
demotions: Array<{
|
|
142
|
+
memoryId: string;
|
|
143
|
+
targetTier: "L2";
|
|
144
|
+
reason: string;
|
|
145
|
+
}>;
|
|
146
|
+
pins: Array<{
|
|
147
|
+
memoryId: string;
|
|
148
|
+
targetTier: "L0";
|
|
149
|
+
reason: string;
|
|
150
|
+
}>;
|
|
151
|
+
detectedTensions: Array<{
|
|
152
|
+
claimA: string;
|
|
153
|
+
claimB: string;
|
|
154
|
+
impact: TensionImpact;
|
|
155
|
+
actionableQuestion: string;
|
|
156
|
+
}>;
|
|
157
|
+
trajectoryPrediction?: TrajectoryPrediction;
|
|
158
|
+
selfModelUpdate?: {
|
|
159
|
+
domain: string;
|
|
160
|
+
success?: boolean;
|
|
161
|
+
failurePatternObserved?: string;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type ArbiterFn = (params: {
|
|
166
|
+
turnText: string;
|
|
167
|
+
assistantReply: string;
|
|
168
|
+
l0Prompt: string;
|
|
169
|
+
l1Summaries: Array<{ id: string; bookmark: string; domains: string[] }>;
|
|
170
|
+
candidates: Array<{ id: string; bookmark: string; domains: string[]; hasTension?: boolean }>;
|
|
171
|
+
}) => Promise<ArbiterEvaluationResult>;
|
|
172
|
+
|
|
173
|
+
export interface CognitiveMemoryOptions {
|
|
174
|
+
/** Maximum token budget for L0 (default: 2000) */
|
|
175
|
+
maxL0Tokens?: number;
|
|
176
|
+
/** Maximum token budget for L1 hot cache (default: 8000) */
|
|
177
|
+
maxL1Tokens?: number;
|
|
178
|
+
/** Ceiling on everything injected into a single prompt (index + bodies). Default 2000. */
|
|
179
|
+
maxTotalTokens?: number;
|
|
180
|
+
/** Custom Arbiter evaluator function (e.g. powered by Jev, Gemini Flash, or local model) */
|
|
181
|
+
arbiter?: ArbiterFn;
|
|
182
|
+
/** Initial self-model */
|
|
183
|
+
initialSelfModel?: Partial<ProprioceptiveSelfModel>;
|
|
184
|
+
/** Auto-extract candidate items from completed turns (default: true) */
|
|
185
|
+
autoExtractMemories?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Adjudicate a candidate memory against ones that already exist.
|
|
188
|
+
*
|
|
189
|
+
* Lexical overlap cannot make this call: it peaks on identical strings and
|
|
190
|
+
* bottoms out on the paraphrases that actually add information. Every mature
|
|
191
|
+
* memory system therefore recalls candidates cheaply and then asks a model,
|
|
192
|
+
* explicitly allowing "no match". `remember` is the low-bar candidate
|
|
193
|
+
* recall; `decide` is the precision step.
|
|
194
|
+
*/
|
|
195
|
+
reconcile?: (input: {
|
|
196
|
+
/** Every candidate from this turn that resembles something we hold. */
|
|
197
|
+
items: Array<{ candidate: string; remember: string[] }>;
|
|
198
|
+
}) => Promise<MemoryReconciliation[]>;
|
|
199
|
+
/**
|
|
200
|
+
* Model-backed turn extractor. Preferred over the built-in regex, which only
|
|
201
|
+
* recognises "always/never/make sure to/remember to" and therefore never
|
|
202
|
+
* learned ordinary project facts. Return an empty array to learn nothing.
|
|
203
|
+
*/
|
|
204
|
+
extract?: (turn: { userMessage: string; assistantResponse: string }) => Promise<{
|
|
205
|
+
memories: Array<{ content: string; domains?: string[] }>;
|
|
206
|
+
tensions?: Array<{
|
|
207
|
+
claimA: string;
|
|
208
|
+
claimB: string;
|
|
209
|
+
impact: "low" | "medium" | "critical";
|
|
210
|
+
actionableQuestion: string;
|
|
211
|
+
}>;
|
|
212
|
+
}>;
|
|
213
|
+
/** Persistence callback to save L2/L3 state */
|
|
214
|
+
onPersist?: (state: CognitiveMemoryStateSnapshot) => Promise<void> | void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface CognitiveMemoryStateSnapshot {
|
|
218
|
+
l0: {
|
|
219
|
+
tensions: KnowledgeTension[];
|
|
220
|
+
selfModel: ProprioceptiveSelfModel;
|
|
221
|
+
activeTaskTrace: string;
|
|
222
|
+
};
|
|
223
|
+
l1: MemoryItem[];
|
|
224
|
+
l2: MemoryItem[];
|
|
225
|
+
l3: MemoryItem[];
|
|
226
|
+
stats: {
|
|
227
|
+
totalTurnsProcessed: number;
|
|
228
|
+
predictionsHit: number;
|
|
229
|
+
predictionsTotal: number;
|
|
230
|
+
tensionsDetected: number;
|
|
231
|
+
};
|
|
232
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CognitiveMemoryErrorBody } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One error class for every failure the service can produce.
|
|
5
|
+
*
|
|
6
|
+
* The service already returns a structured envelope — a tag, a message, the
|
|
7
|
+
* scope that was missing, the fields that failed validation — so the SDK's job is
|
|
8
|
+
* to hand that over as a typed object instead of making callers dig through
|
|
9
|
+
* `error.response.data`.
|
|
10
|
+
*
|
|
11
|
+
* The predicate methods exist so callers ask semantic questions. `status === 403`
|
|
12
|
+
* scatters a magic number through every integration and means something slightly
|
|
13
|
+
* different at each one; `isScopeError()` means the same thing everywhere.
|
|
14
|
+
*/
|
|
15
|
+
export class CognitiveMemoryError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* Set as a real value, because a minifier renames the class and that silently
|
|
18
|
+
* changes what monitoring groups on. `error.name` is correct either way — the
|
|
19
|
+
* constructor assigns it — but `SomeError.name` and `error.constructor.name`
|
|
20
|
+
* are how a reporter identifies a class, and a minified `"h"` breaks that.
|
|
21
|
+
*/
|
|
22
|
+
static override readonly name = "CognitiveMemoryError"
|
|
23
|
+
|
|
24
|
+
/** The service's own tag: `Unauthorized`, `Forbidden`, `InvalidRequest`, … */
|
|
25
|
+
readonly code: string
|
|
26
|
+
readonly status: number
|
|
27
|
+
/** Present on a 403: the scope the key would have needed. */
|
|
28
|
+
readonly requiredScope?: string
|
|
29
|
+
/** Present on a 400: which field, and why. */
|
|
30
|
+
readonly issues?: string[]
|
|
31
|
+
readonly body?: CognitiveMemoryErrorBody
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
message: string,
|
|
35
|
+
init: {
|
|
36
|
+
status: number
|
|
37
|
+
code: string
|
|
38
|
+
body?: CognitiveMemoryErrorBody
|
|
39
|
+
requiredScope?: string
|
|
40
|
+
issues?: string[]
|
|
41
|
+
}
|
|
42
|
+
) {
|
|
43
|
+
super(message)
|
|
44
|
+
this.name = "CognitiveMemoryError"
|
|
45
|
+
this.status = init.status
|
|
46
|
+
this.code = init.code
|
|
47
|
+
this.body = init.body
|
|
48
|
+
this.requiredScope = init.requiredScope
|
|
49
|
+
this.issues = init.issues
|
|
50
|
+
|
|
51
|
+
// Keeps the constructor out of the stack, so the first frame is the caller.
|
|
52
|
+
if (Error.captureStackTrace) {
|
|
53
|
+
Error.captureStackTrace(this, CognitiveMemoryError)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The key is missing, malformed, expired or revoked. */
|
|
58
|
+
isAuthError(): boolean {
|
|
59
|
+
return this.status === 401
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The key is valid but lacks a scope. `requiredScope` says which. */
|
|
63
|
+
isScopeError(): boolean {
|
|
64
|
+
return this.status === 403
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The request body did not match the schema. `issues` says where. */
|
|
68
|
+
isValidationError(): boolean {
|
|
69
|
+
return this.status === 400
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Nothing exists at that id — or it belongs to another organisation. */
|
|
73
|
+
isNotFoundError(): boolean {
|
|
74
|
+
return this.status === 404
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
isRateLimitError(): boolean {
|
|
78
|
+
return this.status === 429
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A dependency of ours failed, not the request. Safe to retry. */
|
|
82
|
+
isServerError(): boolean {
|
|
83
|
+
return this.status >= 500
|
|
84
|
+
}
|
|
85
|
+
}
|