@mnemora/core
v1.2.0
Published
mnemora の core パッケージ。zod 以外の実行時依存を持たない。interface / 型 / 純関数の既定戦略。
Downloads
3,664
Readme
@mnemora/core
mnemora の core パッケージ。型・interface・runtime.observe/tick/recall の実装・
純関数の既定戦略(減衰・スコアリング)を持つ。実行時依存は zod だけ。
インストール
pnpm add @mnemora/core
# または
npm i @mnemora/core前提
文脈を使う抽出
単独では意味が決まらない返答には、observe の extractionContext を渡せます。
文脈は観測と一緒に保存され、非同期抽出や reextract でも使われます。
await runtime.observe(ctx, {
kind: "utterance",
text: "それでお願いします。明日使います",
speaker: "田中",
occurredAt: new Date("2026-01-01T23:00:00Z"),
extractionContext: {
messages: [{ speaker: "assistant", text: "会議室は青葉でよいですか?" }],
timeZone: "Asia/Tokyo",
},
});入力上限は公開 ExtractionContextSchema を参照してください。必要な文脈は呼び手が選びます。
空の {} でも話者・日時を渡す経路を有効にできます。省略時は従来の単独本文の抽出です。
occurredAt と timeZone が揃わなければ、相対日付の確定は指示しません。
文脈を渡しても意味の解釈はモデル依存です。DBの有効期間は従来どおり validFrom/validUntil で明示します。
- Node.js >= 22(
package.jsonのengines) - ESM のみ(
"type": "module")。CommonJS からは Node 22.12 以降のrequire(esm)で読み込める(TypeScript はmodule/moduleResolutionをnodenextにし、TypeScript 5.8 以降を使うこと。 5.7 以前のnodenextと、どの版のnode16もTS1479になる。node10は TypeScript 5.x なら パッケージの入口の型を解決できるが、exportsを読まないので@mnemora/testkit/fixturesのような subpath は解決できず、TypeScript 6 で非推奨・7 で廃止された。2026-09-27 に TypeScript 5.0〜7.0 で実測) - TypeScript の
lib・targetは ES2022 以上。公開の.d.tsがErrorOptions(ES2022 の lib)を使う(memory-store・vector-storeの例外クラス)。 ES2021 以下でskipLibCheck: falseだとTS2304、skipLibCheck: trueだとcauseの型が失われる - 実行時の依存は zod のみ
⚠ @mnemora/core だけでは動く物が組めない
このパッケージは interface・型・純関数が中心で、それ単体では DB にも LLM にも
埋め込みモデルにもつながらない。runtime.observe() / runtime.tick() / runtime.recall() を
実際に動かすには、MemoryStore / VectorStore / EventStore / OutboxStore /
TenantSettingsStore(すべて Postgres 実装は @mnemora/postgres)と、
LLMProvider / EmbeddingProvider(OpenAI 実装は @mnemora/openai、
テスト用の決定的な擬似実装は @mnemora/testkit)を
呼び出し側が用意して渡す必要がある。
import type {
EmbeddingProvider,
EventStore,
LLMProvider,
MemoryStore,
OutboxStore,
TenantSettingsStore,
VectorStore,
} from "@mnemora/core";
import { createRuntime } from "@mnemora/core";
import { createHash } from "node:crypto";
// この7つは @mnemora/core が実装を持たない——ここでは型だけを示す骨格。
// 実際の値は @mnemora/postgres(store)と @mnemora/openai(provider)、
// またはテスト用に @mnemora/testkit の決定的な擬似 provider から調達する。
declare const memoryStore: MemoryStore;
declare const outboxStore: OutboxStore;
declare const vectorStore: VectorStore;
declare const eventStore: EventStore;
declare const tenantSettingsStore: TenantSettingsStore;
declare const llmProvider: LLMProvider;
declare const embeddingProvider: EmbeddingProvider;
const runtime = createRuntime({
memoryStore,
outboxStore,
vectorStore,
eventStore,
tenantSettingsStore,
llmProvider,
embeddingProvider,
// D16: SHA-256 hex 等、content からハッシュを計算する関数。core は計算しない。
hashContent: (content) => createHash("sha256").update(content).digest("hex"),
});
const ctx = { tenantId: "tenant-1" };
const { observationId } = await runtime.observe(ctx, {
kind: "utterance",
text: "明日、京都へ出張する",
speaker: "user",
});
// observe は outbox に積むだけ。索引づけは tick が行う(tick 無しで recall すると memories は空)
await runtime.tick(ctx, { leaseMs: 30_000 });
const recalled = await runtime.recall(ctx, { text: "京都の予定は?" });
console.log(observationId, recalled.memories.length);⚠ observe() は記憶を outbox に積むだけで、索引づけ(埋め込み)は tick() が行う。tick() を呼ばずに recall() すると、memories: [] が返り、omitted に not_indexed(reason: "pending")が出る(インメモリの store と @mnemora/testkit の決定的な provider で、tick 無しは空・tick を1回呼ぶと1件返ることを確かめた)。常駐のワーカーが tick() を回す構成では、この1行は要らない(@mnemora/bullmq)。leaseMs(ジョブを掴む時間)は必須。
配線をすぐ試したいだけなら、@mnemora/postgres と @mnemora/openai の README にある
そのままの例をつなげば動く(@mnemora/postgres 側は本物の Postgres + pgvector が要る)。
⚠ 連想枠(recall() の段3.5)は既定 on(ADR 0337)
recall() は、RecallQuery.association を省略すると DEFAULT_RECALL_ASSOCIATION
({ maxCount: 10 })を適用して連想を走らせる。
⟹ このパッケージを入れたままの既定の振る舞いは「聞かれたことに加えて、関連する記憶も添える」。
(packages/core/src/recall.ts の RecallQuery.association の doc コメント参照。
ADR 0151 が導入した「省略時は連想を
一切走らせない」という既定 off は、ADR 0337
(2026-09-26、オーナーの決定)が反転させた)
一切走らせたくない呼び出しは association: null を明示的に渡す:
const recalled = await runtime.recall(ctx, {
text: "京都の予定は?",
association: null, // 明示的に off にする(省略すると DEFAULT_RECALL_ASSOCIATION が適用される)
});既定値ではなく自分で値を渡したいときは、maxCount を明示する:
const recalled = await runtime.recall(ctx, {
text: "京都の予定は?",
association: { maxCount: 10 },
});maxCount— 必須。既定値は個別には無い(association自体を省略するとDEFAULT_RECALL_ASSOCIATION.maxCount = 10が使われるが、associationオブジェクトを 渡す場合はmaxCountを呼び出し側が明示する必要がある——「量の上限を呼び出し側に 必ず明示させる」という ADR 0151 の設計は維持している)anchorCount?— 段3までに残った上位何件を連想の起点(アンカー)にするか。 既定DEFAULT_ASSOCIATION_ANCHOR_COUNT= 3。 ⚠limit(既定 10)が天井になる——アンカーは段2でlimitの内側に入った候補から取るので、 **anchorCountだけを上げても効かない。**裾野を広げたいならlimitと両方上げること (【実測 2026-09-17】limit:10 / anchorCount:40で実際に起点になったアンカーは 10件、limit:40 / anchorCount:40では 40件。 docs/recall.md §9.2「⚠anchorCountの天井」)minSimilarity?— アンカーとの生のコサイン類似度の下限。 既定DEFAULT_ASSOCIATION_MIN_SIMILARITY= 0.5(scoreThresholdとは尺度が違う別の値)
連想で来た候補は retrievedVia: "association" と associationOf(どのアンカーが連れてきたか)を
持つので、クエリに当たった候補と区別できる。
渡すと何が変わるか。 association-probes ベンチ(probe 12件、本物の Postgres + pgvector、
埋め込みは @mnemora/local-embedding のプロセス内 ONNX 推論)の実測では、maxCount: 10 で
連想でしか届かない gold の到達が 0/12 → 12/12、費用は memoryChars +4.32% だった
(maxCount: 5 では 10/12・+2.22%。
ADR 0168)。
⚠ これはこのリポジトリの probe 12件で測った値であり、他のデータでの値ではない。
⚠ VectorStore.getVectors(任意メソッド)を実装していない adapter では、association を
渡しても連想は走らない——走らなかったことは
stage_skipped { stage: 'association', reason: 'vector_store_lacks_get_vectors' } として
omitted に名乗る(docs/recall.md §9)。
⚠ ctx.subjectId を省略すると「テナント全体」になる(既定はそちら)
subjectId は recall() の引数ではない。Ctx の任意欄である。
const ctx = { tenantId: "tenant-1" }; // ⟹ テナント全体が対象
const scoped = { tenantId: "tenant-1", subjectId: "user-1" }; // ⟹ この subject だけが対象⟹ すべてのメソッドの第一引数に載る任意欄なので、意識して足さないかぎり付かない。
段5(目次帯の集計、MemoryStore.aggregateScope)のコストが、ここで大きく変わる
【実測 2026-09-17、PostgreSQL 17.11 + pgvector 0.8.0、並列無効、digestBand あり、n=20 の中央値】:
| 行数(1テナント) | subjectId 無し | subjectId あり(絞り先 ≈1%) | subjectId あり(絞り先 10行) |
|---:|---:|---:|---:|
| 1,000 | 3.5ms | 1.5ms | 1.5ms |
| 10,000 | 17.0ms | 1.5ms | 1.4ms |
| 100,000 | 165.1ms | 4.0ms | 1.3ms |
絞ったときのコストは、テナント総行数ではなく絞り先の大きさに比例する——10行の subject なら、 テナントが 1,000行でも 100,000行でも 1.3〜1.5ms で変わらない。
⛔ 「だから絞れ」とは言っていない。subjectId は隔離境界ではなく整理の単位であり、
絞れば当然、他の subject の記憶は返らない。どちらを選ぶかは使う側が決めることである。
⚠ 段5 は recall() から無条件に呼ばれる(渡さなくても走る)。
詳しい実測・測っていないこと・上の数字と
docs/recall.md §5 の古い表(100,000行で 45.8ms)との差は、
同 §5「subjectId を省略すると何が起きるか」を見ること。
⚠ 目次帯(digestBandLimit)は既定で返却量の大半を占めうる
帯は digestBandLimit(既定 DEFAULT_DIGEST_BAND_LIMIT)と帯全体の文字数上限
(呼び出し側からは変えられない)の、どちらか先に当たったほうで切れる。 どちらが先に
当たるかは digest の長さ次第で、digestBandLimit を下げても縮まないことがある。
実測・調整のしかたは docs/recall.md §6「目次帯の量を把握し、
調整する」(Issue #413)を見ること。
単体で呼べる純関数(動く最小の例)
一方で、以下は @mnemora/core だけで完結してそのまま実行できる——
記憶の減衰(DecayStrategy)・スコアリング(ScoringStrategy)・時刻(Clock)・
トークン数の推定(TokenCounter)は、DB もネットワークも要らない純関数として公開している。
import {
defaultDecayStrategy,
defaultScoringStrategy,
heuristicTokenCounter,
systemClock,
} from "@mnemora/core";
const now = systemClock.now();
// 半減期720時間(30日)で、記録から時間が経つほど強度が下がる。
const decayed = defaultDecayStrategy.strengthAt(now, {
recordedAt: new Date("2026-01-01T00:00:00Z"),
lastReinforcedAt: null,
strength: 1,
halfLifeHours: 720,
});
// 減衰・タグ一致・鮮度・強度を掛け合わせた合計スコア。
const score = defaultScoringStrategy({
now,
tags: ["work"],
queryTags: ["work"],
occurredAt: new Date("2026-01-01T00:00:00Z"),
recordedAt: new Date("2026-01-01T00:00:00Z"),
lastReinforcedAt: null,
strength: 1,
halfLifeHours: 720,
});
// 文字種で重み付けした粗い推定(CJK 0.9トークン/字・非CJK 0.25トークン/字)。
// counter: "heuristic" を必ず返す(推定値を実測値の顔で返さない、という契約そのもの)。
// ⚠ CJK 以外の非ラテン文字(キリル・タイ・アラビア文字)は依然として過小評価する。
// 厳密さが要るなら TokenCounter を差し替えること(docs/decisions/0083-*.md)。
const { tokens, counter } = heuristicTokenCounter.count("hello world");
console.log({ decayed, total: score.total, tokens, counter });ほかに export しているもの(約束は各 TSDoc)
@mnemora/core は、この README に出てこない名前も多く export している(型・zod スキーマ・既定値・純関数。
何が在るかの正本は scripts/__snapshots__/public-api/core.d.ts で、⛔ ここに数を写さない)。v1.0.0 の後に公開面へ入った名前を、用途ごとに並べる。
| 用途 | 名前 |
|---|---|
| 属性(attributes) | Attributes・AttributesSchema(受け付ける側)・StoredAttributesSchema(格納・伝播する側)、上限の ATTRIBUTES_MAX_KEYS・ATTRIBUTE_KEY_MIN_LENGTH・ATTRIBUTE_KEY_MAX_LENGTH・ATTRIBUTE_VALUE_MAX_LENGTH、統合先へ運ぶ積集合の intersectAttributes |
| 抽出の文脈・主題の候補 | ExtractionContext(と上の ExtractionContextSchema)、SubjectCandidatesInput・sanitizeCandidateSubjectId、extract: 'deferred' と併せたときのエラーの接頭辞 SUBJECT_CANDIDATES_WITH_DEFERRED_EXTRACT_ERROR_PREFIX |
| claim key(主張キー。observe() の claimKey) | ClaimKeyOptions・ClaimKeyOptionsSchema・ClaimKey・ClaimKeySchema・ClaimKeyBatchResult(と ClaimKeyBatchResultSchema)、normalizeClaimKey・normalizeClaimKeyPart・buildClaimKeyPrompt・deriveClaimKeys・DeriveClaimKeysResult、DEFAULT_KNOWN_PREDICATES_FROM_STORE_LIMIT、検出の結果の ContestedDetectionOutcome、extract: 'deferred' と併せたときの CLAIM_KEY_WITH_DEFERRED_EXTRACT_ERROR_PREFIX(docs/memory-model.md §5) |
| taxonomy・テナント設定 | TaxonomyMode・DEFAULT_TAXONOMY_MODE・readTaxonomyMode・writeTaxonomyMode・assertValidTaxonomyMode・TAXONOMY_MODE_INVALID_MESSAGE・TAXONOMY_MODE_UNSUPPORTED_MESSAGE・LabelSummary、保持期間の assertValidEventRetentionKind・EVENT_RETENTION_KIND_INVALID_MESSAGE |
| recall | 段2の時間項の方針 TimeWeightingPolicy・TIME_WEIGHTING_POLICIES・DEFAULT_TIME_WEIGHTING_POLICY、語彙チャンネルの打ち切りの報告 AnnUnreachedSeverity・AnnUnreachedSeveritySchema |
| contested・superseded の後始末 | Runtime.resolveOrphanedContested の ResolveOrphanedContestedOptions・ResolveOrphanedContestedResult・ResolveOrphanedContestedOutcome・ResolveOrphanedContestedEligibility、restoreSuperseded の dryRun の候補を操作ごとに束ねる groupSupersededCandidatesByOperation・SupersededOperationGroup(docs/memory-model.md §11 行15) |
| テナント消去 | あるテナントに属する行を跡形なく消す独立関数 eraseTenant(Runtime のメソッドではない。tick()/observe() には配線しない、明示呼び出し専用——purgeExpiredEventsForTenant と同じ置き方)と、その EraseTenantOptions・EraseTenantOutcome・EraseTenantDeps。MemoryStore/VectorStore/OutboxStore/TenantSettingsStore それぞれの任意メソッド eraseTenant? とその EraseTenantStoreOptions・EraseTenantResult・(MemoryStore のみ)EraseTenantStoreResult。詳細は docs/memory-model.md §9・ADR 0383 |
もっと詳しく
- docs/architecture.md — 全体アーキテクチャ・主要 interface(§5)
- docs/recall.md —
recall()の7段パイプライン - docs/memory-model.md — Memory のライフサイクル・減衰・忘却
- リポジトリ: https://github.com/takecchi/mnemora
