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.
@@ -0,0 +1,172 @@
1
+ import type { Cogmem } from "../cogmem"
2
+ import type { ContextReport, LearnResult, Memory } from "../types"
3
+
4
+ /**
5
+ * Workflows.
6
+ *
7
+ * Resource methods are one call each. These compose them into the sequences
8
+ * people actually get wrong when they write the loop themselves — and the
9
+ * mistakes are mundane: forgetting to learn from a turn, learning from a question,
10
+ * letting a memory failure take down the turn it was meant to help, or never
11
+ * telling the service how a task went so the guardrails never fire.
12
+ *
13
+ * Each helper takes the client explicitly rather than closing over one, so it
14
+ * works with a second client without a module-level singleton.
15
+ */
16
+
17
+ export interface TurnOptions {
18
+ /** Groups everything learned in one conversation. */
19
+ sessionId?: string
20
+ /**
21
+ * What the turn turned out to be about, e.g. `"database"`.
22
+ *
23
+ * Recorded against the self-model, which is what eventually produces a
24
+ * guardrail for a domain the agent keeps failing in. Cheap to send and the
25
+ * difference between a self-model that learns and one that sits at its priors.
26
+ */
27
+ domain?: string
28
+ /** What went wrong, when the turn was a failure. */
29
+ failurePattern?: string
30
+ /** What worked, when the turn was a success. */
31
+ strategy?: string
32
+ /** Set false to skip learning — a retrieval-only turn, say. */
33
+ learn?: boolean
34
+ onLearn?: (result: LearnResult) => void
35
+ }
36
+
37
+ export interface RunTurnResult {
38
+ context: ContextReport
39
+ learning: LearnResult | null
40
+ /**
41
+ * Why learning did not happen, if it did not.
42
+ *
43
+ * Present because a silent skip looks identical to a working one until the
44
+ * thing you taught it never comes back.
45
+ */
46
+ learningSkipped?: string
47
+ }
48
+
49
+ /**
50
+ * One turn of an agent loop, with memory handled correctly.
51
+ *
52
+ * The order is the point:
53
+ *
54
+ * 1. build context *before* the model runs, keyed on the message being answered;
55
+ * 2. run the model;
56
+ * 3. learn from the finished exchange;
57
+ * 4. record how the domain went, so the self-model moves.
58
+ *
59
+ * Steps 3 and 4 never fail the turn. Memory is an enhancement; an outage in it
60
+ * must not take down the agent that was working fine without it, and the errors
61
+ * are surfaced through `onLearn` and the returned skip reason rather than
62
+ * swallowed.
63
+ */
64
+ export async function runTurn(
65
+ memory: Cogmem,
66
+ input: {
67
+ userMessage: string
68
+ run: (context: string) => Promise<string>
69
+ },
70
+ options: TurnOptions = {}
71
+ ): Promise<RunTurnResult> {
72
+ const context = await memory.context.build({
73
+ userMessage: input.userMessage,
74
+ ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId })
75
+ })
76
+
77
+ const assistantResponse = await input.run(context.text)
78
+
79
+ if (options.learn === false) {
80
+ return { context, learning: null, learningSkipped: "learning disabled for this turn" }
81
+ }
82
+
83
+ let learning: LearnResult | null = null
84
+ try {
85
+ learning = await memory.turns.learn({
86
+ userMessage: input.userMessage,
87
+ assistantResponse,
88
+ ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId })
89
+ })
90
+ options.onLearn?.(learning)
91
+ } catch (error) {
92
+ // Deliberately not rethrown: the turn already succeeded, and the assistant's
93
+ // answer does not become wrong because memory could not be written.
94
+ console.warn("[cognitive-memory] could not learn from this turn", error)
95
+ return { context, learning: null, learningSkipped: String(error) }
96
+ }
97
+
98
+ if (options.domain !== undefined) {
99
+ try {
100
+ await memory.selfModel.record({
101
+ domain: options.domain,
102
+ success: options.failurePattern === undefined,
103
+ ...(options.failurePattern === undefined ? {} : { failurePattern: options.failurePattern }),
104
+ ...(options.strategy === undefined ? {} : { strategy: options.strategy })
105
+ })
106
+ } catch (error) {
107
+ console.warn("[cognitive-memory] could not record the domain outcome", error)
108
+ }
109
+ }
110
+
111
+ return { context, learning }
112
+ }
113
+
114
+ export interface RecallOrExplainOptions {
115
+ limit?: number
116
+ /** Overrides the default "you were not told this" wording. */
117
+ emptyMessage?: string
118
+ }
119
+
120
+ /**
121
+ * Recall, phrased for injection into a prompt.
122
+ *
123
+ * Returns text rather than results because the caller's next step is almost
124
+ * always "put this in the prompt", and doing the formatting here means every
125
+ * integration handles the empty case the same way: by saying so, rather than
126
+ * letting a model fill the gap with a guess.
127
+ */
128
+ export async function recallOrExplain(
129
+ memory: Cogmem,
130
+ query: string,
131
+ options: RecallOrExplainOptions = {}
132
+ ): Promise<string> {
133
+ const response = await memory.recall.search({
134
+ query,
135
+ ...(options.limit === undefined ? {} : { limit: options.limit })
136
+ })
137
+
138
+ if (response.empty) {
139
+ return options.emptyMessage ?? `Nothing in memory matches "${query}". If you were not told, say so rather than guessing.`
140
+ }
141
+
142
+ const lines = response.results.map((hit) => {
143
+ const domains = hit.memory.domains.length > 0 ? ` (${hit.memory.domains.join(", ")})` : ""
144
+ return `- ${hit.memory.content}${domains} [${hit.memory.tier}, relevance ${hit.score.toFixed(2)}]`
145
+ })
146
+ return `Remembered (${response.results.length} match${response.results.length === 1 ? "" : "es"}):\n${lines.join("\n")}`
147
+ }
148
+
149
+ export interface SeedOptions {
150
+ /** Statements worth keeping regardless of how they are phrased. */
151
+ facts: string[]
152
+ sessionId?: string
153
+ }
154
+
155
+ /**
156
+ * Store a set of statements once, tolerating the ones already held.
157
+ *
158
+ * For a migration or a first run, where you have a list of facts and no way to
159
+ * know which the service already knows. Restatements come back under
160
+ * `mergedInto` rather than as errors, so re-running it is safe.
161
+ */
162
+ export async function seedMemories(memory: Cogmem, options: SeedOptions): Promise<{
163
+ stored: Memory[]
164
+ merged: Memory[]
165
+ rejected: LearnResult["rejected"]
166
+ }> {
167
+ const result = await memory.memories.create({
168
+ items: options.facts.map((content) => ({ content })),
169
+ ...(options.sessionId === undefined ? {} : { sessionId: options.sessionId })
170
+ })
171
+ return { stored: result.stored, merged: result.mergedInto, rejected: result.rejected }
172
+ }
package/src/index.ts ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @astracollab/cogmem — Cognitive Memory for agents.
3
+ *
4
+ * Two things in one package, deliberately:
5
+ *
6
+ * 1. **A client** for the Cognitive Memory service, for agents that call memory
7
+ * over HTTP.
8
+ * 2. **The cognitive layer itself** — the deterministic decisions about what to
9
+ * store, what to inject, and whether a new statement is new. No database, no
10
+ * network, no model in the retrieval path.
11
+ *
12
+ * Shipping both means a consumer can start with the service and later run the
13
+ * same logic in-process, or run it only in-process, without the behaviour
14
+ * changing underneath them. The primitives are the reason the two cannot drift.
15
+ *
16
+ * The exports are arranged for tree-shaking: importing `createClient` alone does
17
+ * not pull in the in-process engine, and the model-backed arbiter — which needs
18
+ * `ai` and `zod` — is a separate entry point so those stay optional peers.
19
+ */
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // The service client
23
+ // ---------------------------------------------------------------------------
24
+ export { Cogmem, createClient } from "./cogmem"
25
+ export { createHttpClient, type HttpClient } from "./client"
26
+ export { CognitiveMemoryError } from "./errors"
27
+
28
+ // Helpers — the workflows that encode best practice
29
+ export {
30
+ runTurn,
31
+ recallOrExplain,
32
+ seedMemories,
33
+ type TurnOptions,
34
+ type RunTurnResult,
35
+ type RecallOrExplainOptions,
36
+ type SeedOptions
37
+ } from "./helpers/turn"
38
+
39
+ // Resources, for advanced use and for testing
40
+ export { MemoriesResource } from "./resources/memories"
41
+ export { ContextResource } from "./resources/context"
42
+ export { RecallResource } from "./resources/recall"
43
+ export { TurnsResource } from "./resources/turns"
44
+ export { TensionsResource } from "./resources/tensions"
45
+ export { SelfModelResource } from "./resources/self-model"
46
+ export { StatsResource, fetchHealth } from "./resources/stats"
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // The cognitive layer — runnable in-process
50
+ // ---------------------------------------------------------------------------
51
+ export { CognitiveMemory, runFastGate, extractDomains } from "./cognitive"
52
+ export {
53
+ extractDeterministic,
54
+ relevanceTokens,
55
+ overlapScore,
56
+ distinctiveTokens,
57
+ isLossyRewrite,
58
+ extractIdentifiers,
59
+ isInteractionScoped,
60
+ similarity,
61
+ normalise,
62
+ estimateTokens,
63
+ gistOf,
64
+ CANDIDATE_FLOOR,
65
+ COLLAPSE_THRESHOLD,
66
+ MAX_CANDIDATES,
67
+ PROMOTE_THRESHOLD
68
+ } from "./cognitive"
69
+ export type { FastGateResult, DeterministicMemory } from "./cognitive"
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Types
73
+ // ---------------------------------------------------------------------------
74
+
75
+ // The client's wire types.
76
+ export type {
77
+ Memory,
78
+ MemoryTier,
79
+ MemorySource,
80
+ Impact,
81
+ TensionStatus,
82
+ InclusionReason,
83
+ Scope,
84
+ RecallHit,
85
+ ContextEntry,
86
+ ContextReport,
87
+ LearnResult,
88
+ RejectedStatement,
89
+ Claim,
90
+ Tension,
91
+ DomainCapability,
92
+ SelfModel,
93
+ Health,
94
+ Stats,
95
+ RememberRequest,
96
+ ListMemoriesParams,
97
+ RecallRequest,
98
+ RecallResponse,
99
+ TurnRequest,
100
+ ContextRequest,
101
+ AddTensionRequest,
102
+ ResolveTensionRequest,
103
+ DomainOutcomeRequest,
104
+ CognitiveMemoryConfig,
105
+ CognitiveMemoryErrorBody
106
+ } from "./types"
107
+
108
+ /*
109
+ * The cognitive layer's own vocabulary, under its own names.
110
+ *
111
+ * No prefixing was necessary: the client's types are `Memory`, `Impact`,
112
+ * `Tension` and `SelfModel`, and the layer's are `MemoryItem`, `TensionImpact`,
113
+ * `KnowledgeTension` and `ProprioceptiveSelfModel`. The only genuine overlap is
114
+ * `MemoryTier`, `TensionStatus` and `DomainCapability`, which are identical in
115
+ * both — the same field names and the same string values — so the client's
116
+ * copies above are the canonical ones and are not re-exported here.
117
+ */
118
+ export type {
119
+ MemoryItem,
120
+ MemoryMetadata,
121
+ MemoryInclusionReason,
122
+ MemoryInjectionEntry,
123
+ MemoryInjectionReport,
124
+ MemoryReconciliation,
125
+ MemoryTier as CognitiveMemoryTier,
126
+ KnowledgeTension,
127
+ ProprioceptiveSelfModel,
128
+ TrajectoryPrediction,
129
+ TensionImpact,
130
+ ArbiterEvaluationResult,
131
+ ArbiterFn,
132
+ CognitiveMemoryOptions,
133
+ CognitiveMemoryStateSnapshot
134
+ } from "./cognitive"
@@ -0,0 +1,17 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { ContextReport, ContextRequest } from "../types"
3
+
4
+ /**
5
+ * The prompt block.
6
+ *
7
+ * One call per turn, and the reason the service exists: a one-line index of
8
+ * everything remembered, plus full bodies only where a named identifier, an
9
+ * unresolved contradiction, or a weak domain earned them.
10
+ */
11
+ export class ContextResource {
12
+ constructor(private readonly client: HttpClient) {}
13
+
14
+ build(request: ContextRequest = {}): Promise<ContextReport> {
15
+ return this.client<ContextReport>("/context", { method: "POST", body: request })
16
+ }
17
+ }
@@ -0,0 +1,60 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { LearnResult, ListMemoriesParams, Memory, MemoryTier, RememberRequest } from "../types"
3
+
4
+ /**
5
+ * Reading and writing what is stored.
6
+ *
7
+ * Deliberately thin: one method, one endpoint, no cleverness. The class is a
8
+ * namespace, so `memory.memories.` offers the whole surface in an editor, but it
9
+ * is not a place for business logic. The workflow that composes these calls lives
10
+ * in `../helpers`.
11
+ */
12
+ export class MemoriesResource {
13
+ constructor(private readonly client: HttpClient) {}
14
+
15
+ /**
16
+ * The envelope is unwrapped here rather than passed on.
17
+ *
18
+ * The service replies with `{ memories, stats }` so a dashboard can get both in
19
+ * one call, but an SDK caller who asked for a list wants a list. Deciding that
20
+ * in one place here is better than making every integration know the shape.
21
+ */
22
+ async list(params: ListMemoriesParams = {}): Promise<Memory[]> {
23
+ const response = await this.client<{ memories: Memory[] }>("/memories", {
24
+ method: "GET",
25
+ query: params
26
+ })
27
+ return response.memories
28
+ }
29
+
30
+ async get(id: string): Promise<Memory> {
31
+ const response = await this.client<{ memory: Memory }>(`/memories/${encodeURIComponent(id)}`, {
32
+ method: "GET"
33
+ })
34
+ return response.memory
35
+ }
36
+
37
+ /**
38
+ * State facts outright.
39
+ *
40
+ * Restatements of something already held are folded in rather than stored
41
+ * twice, and the result says which ones were folded and into what — a write
42
+ * that silently did nothing is indistinguishable from a bug.
43
+ */
44
+ create(request: RememberRequest): Promise<LearnResult> {
45
+ return this.client<LearnResult>("/memories", { method: "POST", body: request })
46
+ }
47
+
48
+ /** Move a memory between tiers. L1 is pre-staged into every context build. */
49
+ async promote(id: string, tier: MemoryTier): Promise<Memory> {
50
+ const response = await this.client<{ memory: Memory }>(`/memories/${encodeURIComponent(id)}`, {
51
+ method: "PATCH",
52
+ body: { tier }
53
+ })
54
+ return response.memory
55
+ }
56
+
57
+ remove(id: string): Promise<{ deleted: boolean; id: string }> {
58
+ return this.client(`/memories/${encodeURIComponent(id)}`, { method: "DELETE" })
59
+ }
60
+ }
@@ -0,0 +1,17 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { RecallRequest, RecallResponse } from "../types"
3
+
4
+ /**
5
+ * Deterministic ranked lookup.
6
+ *
7
+ * No model is involved, so recall quality does not change when a provider is
8
+ * down or a different model is configured. `empty: true` is the signal to admit
9
+ * ignorance rather than guess.
10
+ */
11
+ export class RecallResource {
12
+ constructor(private readonly client: HttpClient) {}
13
+
14
+ search(request: RecallRequest): Promise<RecallResponse> {
15
+ return this.client<RecallResponse>("/recall", { method: "POST", body: request })
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { DomainOutcomeRequest, SelfModel } from "../types"
3
+
4
+ /**
5
+ * The proprioceptive self-model: how reliably this agent has done in each domain.
6
+ *
7
+ * Nothing here is inferred. Without outcomes recorded through `record`, the
8
+ * model stays at its priors and no guardrail ever fires — which is the most
9
+ * common reason a self-model looks like it is not working.
10
+ */
11
+ export class SelfModelResource {
12
+ constructor(private readonly client: HttpClient) {}
13
+
14
+ get(): Promise<SelfModel> {
15
+ return this.client<SelfModel>("/self-model", { method: "GET" })
16
+ }
17
+
18
+ record(request: DomainOutcomeRequest): Promise<SelfModel> {
19
+ return this.client<SelfModel>("/self-model/outcome", { method: "POST", body: request })
20
+ }
21
+ }
@@ -0,0 +1,27 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { Health, Stats } from "../types"
3
+
4
+ /** What the memory currently holds, and what the service can do. */
5
+ export class StatsResource {
6
+ constructor(private readonly client: HttpClient) {}
7
+
8
+ /** Needs only `stats:read`, so monitoring can watch without reading memories. */
9
+ get(): Promise<Stats> {
10
+ return this.client<Stats>("/stats", { method: "GET" })
11
+ }
12
+ }
13
+
14
+ /**
15
+ * Liveness, and this deployment's limits and extractor mode.
16
+ *
17
+ * Takes an explicit base URL because it is the one call that must work with no
18
+ * key at all — including from a health check that has no credentials.
19
+ */
20
+ export const fetchHealth = async (baseUrl: string): Promise<Health> => {
21
+ const origin = baseUrl.replace(/\/$/, "")
22
+ const response = await fetch(`${origin}/api/v1/health`, { headers: { accept: "application/json" } })
23
+ if (!response.ok) {
24
+ throw new Error(`cognitive-memory: health check failed with ${response.status}`)
25
+ }
26
+ return (await response.json()) as Health
27
+ }
@@ -0,0 +1,36 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { AddTensionRequest, ResolveTensionRequest, Tension, TensionStatus } from "../types"
3
+
4
+ /**
5
+ * Knowledge tensions: two claims that cannot both be true.
6
+ *
7
+ * A tension is worth more than either claim alone, so it is pinned into every
8
+ * context build with an actionable question until it is resolved.
9
+ */
10
+ export class TensionsResource {
11
+ constructor(private readonly client: HttpClient) {}
12
+
13
+ async list(status?: TensionStatus): Promise<Tension[]> {
14
+ const response = await this.client<{ tensions: Tension[] }>("/tensions", {
15
+ method: "GET",
16
+ query: status === undefined ? {} : { status }
17
+ })
18
+ return response.tensions
19
+ }
20
+
21
+ async create(request: AddTensionRequest): Promise<Tension> {
22
+ const response = await this.client<{ tension: Tension }>("/tensions", {
23
+ method: "POST",
24
+ body: request
25
+ })
26
+ return response.tension
27
+ }
28
+
29
+ async resolve(id: string, request: ResolveTensionRequest): Promise<Tension> {
30
+ const response = await this.client<{ tension: Tension }>(`/tensions/${encodeURIComponent(id)}`, {
31
+ method: "POST",
32
+ body: request
33
+ })
34
+ return response.tension
35
+ }
36
+ }
@@ -0,0 +1,16 @@
1
+ import type { HttpClient } from "../client"
2
+ import type { LearnResult, TurnRequest } from "../types"
3
+
4
+ /**
5
+ * Learning from a completed turn.
6
+ *
7
+ * A turn whose user message contains a question is treated as a lookup rather
8
+ * than a lesson, so this is called after work is done, not before.
9
+ */
10
+ export class TurnsResource {
11
+ constructor(private readonly client: HttpClient) {}
12
+
13
+ learn(request: TurnRequest): Promise<LearnResult> {
14
+ return this.client<LearnResult>("/turns", { method: "POST", body: request })
15
+ }
16
+ }