@graphorin/memory 0.6.0 → 0.7.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/CHANGELOG.md +83 -0
- package/README.md +24 -7
- package/dist/conflict/index.js +0 -1
- package/dist/conflict/pipeline.js +0 -1
- package/dist/conflict/pipeline.js.map +1 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
- package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
- package/dist/conflict/types.d.ts +1 -1
- package/dist/conflict/types.js.map +1 -1
- package/dist/consolidator/index.js +2 -0
- package/dist/consolidator/index.js.map +1 -1
- package/dist/consolidator/lock.js +7 -1
- package/dist/consolidator/lock.js.map +1 -1
- package/dist/consolidator/phases/deep.js +19 -12
- package/dist/consolidator/phases/deep.js.map +1 -1
- package/dist/consolidator/phases/induce.d.ts.map +1 -1
- package/dist/consolidator/phases/induce.js +2 -3
- package/dist/consolidator/phases/induce.js.map +1 -1
- package/dist/consolidator/phases/reflect.js +35 -8
- package/dist/consolidator/phases/reflect.js.map +1 -1
- package/dist/consolidator/phases/standard.js +199 -28
- package/dist/consolidator/phases/standard.js.map +1 -1
- package/dist/consolidator/reconcile.js +16 -15
- package/dist/consolidator/reconcile.js.map +1 -1
- package/dist/consolidator/runtime.d.ts.map +1 -1
- package/dist/consolidator/runtime.js +129 -47
- package/dist/consolidator/runtime.js.map +1 -1
- package/dist/consolidator/scheduler.d.ts +17 -3
- package/dist/consolidator/scheduler.d.ts.map +1 -1
- package/dist/consolidator/scheduler.js.map +1 -1
- package/dist/consolidator/types.d.ts +30 -1
- package/dist/consolidator/types.d.ts.map +1 -1
- package/dist/consolidator/types.js +10 -0
- package/dist/consolidator/types.js.map +1 -1
- package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js +1 -1
- package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
- package/dist/context-engine/compaction/compactor.d.ts +1 -0
- package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
- package/dist/context-engine/compaction/compactor.js +32 -8
- package/dist/context-engine/compaction/compactor.js.map +1 -1
- package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
- package/dist/context-engine/compaction/index.d.ts +1 -0
- package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
- package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
- package/dist/context-engine/compaction/templates/marker.js +34 -0
- package/dist/context-engine/compaction/templates/marker.js.map +1 -0
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
- package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
- package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
- package/dist/context-engine/engine.d.ts +7 -176
- package/dist/context-engine/engine.d.ts.map +1 -1
- package/dist/context-engine/engine.js +0 -2
- package/dist/context-engine/engine.js.map +1 -1
- package/dist/context-engine/index.d.ts +8 -49
- package/dist/context-engine/io-types.d.ts +144 -0
- package/dist/context-engine/io-types.d.ts.map +1 -0
- package/dist/context-engine/locale-packs/types.d.ts +1 -1
- package/dist/context-engine/locale-packs/types.js.map +1 -1
- package/dist/context-engine/privacy-filter.d.ts +1 -2
- package/dist/context-engine/privacy-filter.d.ts.map +1 -1
- package/dist/context-engine/privacy-filter.js +1 -2
- package/dist/context-engine/privacy-filter.js.map +1 -1
- package/dist/errors/index.d.ts +2 -2
- package/dist/errors/index.js +2 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/facade.d.ts +43 -52
- package/dist/facade.d.ts.map +1 -1
- package/dist/facade.js +5 -3
- package/dist/facade.js.map +1 -1
- package/dist/graph/entity-resolver.js +1 -1
- package/dist/graph/entity-resolver.js.map +1 -1
- package/dist/index.d.ts +17 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/envelope.js +57 -0
- package/dist/internal/envelope.js.map +1 -0
- package/dist/internal/injection-heuristics.js +0 -0
- package/dist/internal/injection-heuristics.js.map +1 -1
- package/dist/internal/llm-json.js +40 -0
- package/dist/internal/llm-json.js.map +1 -0
- package/dist/internal/storage-adapter.d.ts +23 -10
- package/dist/internal/storage-adapter.d.ts.map +1 -1
- package/dist/memory-interface.d.ts +131 -0
- package/dist/memory-interface.d.ts.map +1 -0
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/search/iterative.d.ts.map +1 -1
- package/dist/search/iterative.js +2 -3
- package/dist/search/iterative.js.map +1 -1
- package/dist/search/query-transform.d.ts.map +1 -1
- package/dist/search/query-transform.js +2 -3
- package/dist/search/query-transform.js.map +1 -1
- package/dist/tiers/episodic-memory.js +3 -3
- package/dist/tiers/episodic-memory.js.map +1 -1
- package/dist/tiers/index.d.ts +2 -2
- package/dist/tiers/insight-memory.js +1 -1
- package/dist/tiers/insight-memory.js.map +1 -1
- package/dist/tiers/procedural-memory.d.ts +1 -1
- package/dist/tiers/procedural-memory.js +2 -2
- package/dist/tiers/procedural-memory.js.map +1 -1
- package/dist/tiers/semantic-memory.d.ts +58 -4
- package/dist/tiers/semantic-memory.d.ts.map +1 -1
- package/dist/tiers/semantic-memory.js +103 -20
- package/dist/tiers/semantic-memory.js.map +1 -1
- package/dist/tools/block-tools.d.ts +19 -57
- package/dist/tools/block-tools.d.ts.map +1 -1
- package/dist/tools/block-tools.js.map +1 -1
- package/dist/tools/fact-tools.d.ts +53 -214
- package/dist/tools/fact-tools.d.ts.map +1 -1
- package/dist/tools/fact-tools.js +3 -2
- package/dist/tools/fact-tools.js.map +1 -1
- package/dist/tools/recall-tools.d.ts +34 -175
- package/dist/tools/recall-tools.d.ts.map +1 -1
- package/dist/tools/recall-tools.js +5 -4
- package/dist/tools/recall-tools.js.map +1 -1
- package/dist/tools/runbook-tools.d.ts +15 -58
- package/dist/tools/runbook-tools.d.ts.map +1 -1
- package/dist/tools/runbook-tools.js.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/package.json +18 -17
- package/src/conflict/index.ts +29 -0
- package/src/conflict/locale-packs/en.ts +142 -0
- package/src/conflict/locale-packs/index.ts +17 -0
- package/src/conflict/locale-packs/types.ts +138 -0
- package/src/conflict/pipeline.ts +371 -0
- package/src/conflict/stages/helpers.ts +65 -0
- package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
- package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
- package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
- package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
- package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
- package/src/conflict/types.ts +263 -0
- package/src/consolidator/budget.ts +312 -0
- package/src/consolidator/decay.ts +246 -0
- package/src/consolidator/dlq.ts +61 -0
- package/src/consolidator/errors.ts +87 -0
- package/src/consolidator/idempotency.ts +27 -0
- package/src/consolidator/index.ts +269 -0
- package/src/consolidator/lock.ts +141 -0
- package/src/consolidator/noise-filter.ts +235 -0
- package/src/consolidator/phases/deep.ts +308 -0
- package/src/consolidator/phases/induce.ts +423 -0
- package/src/consolidator/phases/learned-context.ts +233 -0
- package/src/consolidator/phases/light.ts +166 -0
- package/src/consolidator/phases/reflect.ts +504 -0
- package/src/consolidator/phases/standard.ts +1002 -0
- package/src/consolidator/reconcile.ts +273 -0
- package/src/consolidator/runtime.ts +1049 -0
- package/src/consolidator/scheduler.ts +182 -0
- package/src/consolidator/triggers.ts +134 -0
- package/src/consolidator/types.ts +622 -0
- package/src/context-engine/annotations.ts +161 -0
- package/src/context-engine/auto-recall.ts +92 -0
- package/src/context-engine/compaction/clear-tool-results.ts +232 -0
- package/src/context-engine/compaction/compactor.ts +403 -0
- package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
- package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
- package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
- package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
- package/src/context-engine/compaction/hooks/types.ts +68 -0
- package/src/context-engine/compaction/index.ts +60 -0
- package/src/context-engine/compaction/templates/marker.ts +33 -0
- package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
- package/src/context-engine/compaction/thresholds.ts +107 -0
- package/src/context-engine/compaction/types.ts +255 -0
- package/src/context-engine/engine.ts +970 -0
- package/src/context-engine/index.ts +118 -0
- package/src/context-engine/io-types.ts +162 -0
- package/src/context-engine/locale-packs/en.ts +107 -0
- package/src/context-engine/locale-packs/index.ts +19 -0
- package/src/context-engine/locale-packs/resolver.ts +197 -0
- package/src/context-engine/locale-packs/types.ts +177 -0
- package/src/context-engine/metadata.ts +114 -0
- package/src/context-engine/preambles/inbound-en.ts +19 -0
- package/src/context-engine/privacy-filter.ts +190 -0
- package/src/context-engine/templates/base-en.ts +19 -0
- package/src/context-engine/templates/composer.ts +91 -0
- package/src/context-engine/token-budget.ts +319 -0
- package/src/context-engine/token-counter.ts +157 -0
- package/src/errors/index.ts +273 -0
- package/src/facade.ts +750 -0
- package/src/graph/entity-resolver.ts +364 -0
- package/src/graph/index.ts +24 -0
- package/src/index.ts +259 -0
- package/src/internal/contextualize.ts +173 -0
- package/src/internal/embedder-binding.ts +53 -0
- package/src/internal/envelope.ts +70 -0
- package/src/internal/id.ts +23 -0
- package/src/internal/injection-heuristics.ts +0 -0
- package/src/internal/llm-json.ts +38 -0
- package/src/internal/spans.ts +35 -0
- package/src/internal/storage-adapter.ts +963 -0
- package/src/memory-interface.ts +158 -0
- package/src/migration/embedder-migration.ts +237 -0
- package/src/migration/index.ts +13 -0
- package/src/search/explain.ts +123 -0
- package/src/search/fit-weights.ts +109 -0
- package/src/search/graph-ppr.ts +38 -0
- package/src/search/index.ts +55 -0
- package/src/search/iterative.ts +658 -0
- package/src/search/query-transform.ts +285 -0
- package/src/search/rrf.ts +225 -0
- package/src/search/trust.ts +55 -0
- package/src/search/types.ts +50 -0
- package/src/tiers/episodic-memory.ts +463 -0
- package/src/tiers/index.ts +50 -0
- package/src/tiers/insight-memory.ts +208 -0
- package/src/tiers/procedural-memory.ts +482 -0
- package/src/tiers/semantic-memory.ts +1729 -0
- package/src/tiers/session-memory.ts +271 -0
- package/src/tiers/shared-memory.ts +62 -0
- package/src/tiers/working-memory.ts +368 -0
- package/src/tools/block-tools.ts +172 -0
- package/src/tools/fact-tools.ts +473 -0
- package/src/tools/index.ts +109 -0
- package/src/tools/recall-tools.ts +303 -0
- package/src/tools/runbook-tools.ts +107 -0
- package/src/tools/taint.ts +39 -0
- package/src/tools/types.ts +37 -0
- package/dist/conflict/locale-packs/index.js +0 -4
- package/dist/context-engine/compaction/index.js +0 -10
- package/dist/context-engine/index.d.ts.map +0 -1
- package/dist/context-engine/index.js +0 -25
- package/dist/context-engine/locale-packs/index.js +0 -4
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ContextEngine` - the layered six-layer system prompt assembler.
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
*
|
|
6
|
+
* - Layer 1 (`graphorin_memory_base`) educational template
|
|
7
|
+
* - Layer 2 user-defined `agent_instructions`
|
|
8
|
+
* - Layer 3 working-memory blocks (filtered through D2 privacy)
|
|
9
|
+
* - Layer 4 procedural rules + skill metadata cards
|
|
10
|
+
* - Layer 5 memory-metadata counters
|
|
11
|
+
* - Layer 6 opt-in auto-recall
|
|
12
|
+
*
|
|
13
|
+
* Cross-cuts:
|
|
14
|
+
*
|
|
15
|
+
* - D2 privacy filter (sensitivity-tier drop)
|
|
16
|
+
* - D3 cooperation contract (`ContentOrigin` annotation)
|
|
17
|
+
* - D4 inbound preamble injection (post cache-breakpoint)
|
|
18
|
+
* - RB-44 per-step tool-catalogue cardinality allocator (separate axis)
|
|
19
|
+
* - RB-46 in-flight session message-history compaction (separate axis)
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
Fact,
|
|
26
|
+
LocalProviderTrust,
|
|
27
|
+
Message,
|
|
28
|
+
MessageContent,
|
|
29
|
+
Sensitivity,
|
|
30
|
+
SessionScope,
|
|
31
|
+
TokenCounter,
|
|
32
|
+
} from '@graphorin/core';
|
|
33
|
+
import type { ContextEngine, Memory } from '../memory-interface.js';
|
|
34
|
+
import { annotate, type ContentAnnotation, shouldFireInboundPreamble } from './annotations.js';
|
|
35
|
+
import {
|
|
36
|
+
type AutoRecallStrategy,
|
|
37
|
+
type AutoRecallTriggerResult,
|
|
38
|
+
defaultLocaleHeuristicStrategy,
|
|
39
|
+
} from './auto-recall.js';
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_PRESERVE_RECENT_TURNS,
|
|
42
|
+
type ExecuteCompactionInput,
|
|
43
|
+
executeCompaction,
|
|
44
|
+
} from './compaction/compactor.js';
|
|
45
|
+
import type { NamedPostCompactionHook } from './compaction/hooks/types.js';
|
|
46
|
+
import {
|
|
47
|
+
reanchorPersonaBlock,
|
|
48
|
+
reanchorPinnedFacts,
|
|
49
|
+
reanchorProjectRules,
|
|
50
|
+
} from './compaction/index.js';
|
|
51
|
+
import { resolveAutoCompactionDefault, resolveTriggerThreshold } from './compaction/thresholds.js';
|
|
52
|
+
import type {
|
|
53
|
+
CompactionConfig,
|
|
54
|
+
CompactionResult,
|
|
55
|
+
CompactionSource,
|
|
56
|
+
CompactionStrategy,
|
|
57
|
+
CompactionSummarizer,
|
|
58
|
+
PostCompactionHook,
|
|
59
|
+
} from './compaction/types.js';
|
|
60
|
+
import type {
|
|
61
|
+
AnnotatedPart,
|
|
62
|
+
AssembledPrompt,
|
|
63
|
+
AssembleInput,
|
|
64
|
+
ResolvedContextEngineConfig,
|
|
65
|
+
} from './io-types.js';
|
|
66
|
+
import {
|
|
67
|
+
type ContextLocalePack,
|
|
68
|
+
enLocalePack,
|
|
69
|
+
type PartialContextLocalePack,
|
|
70
|
+
} from './locale-packs/index.js';
|
|
71
|
+
import { resolveLocalePack } from './locale-packs/resolver.js';
|
|
72
|
+
import { renderMetadataBlock } from './metadata.js';
|
|
73
|
+
import { type PrivacyDecisionReason, decide as privacyDecide } from './privacy-filter.js';
|
|
74
|
+
import {
|
|
75
|
+
composeInboundPreamble,
|
|
76
|
+
composeLayer1,
|
|
77
|
+
composeLayer2,
|
|
78
|
+
composeLayer4Skills,
|
|
79
|
+
type MemoryBaseMode,
|
|
80
|
+
} from './templates/composer.js';
|
|
81
|
+
import {
|
|
82
|
+
allocate as allocateLayers,
|
|
83
|
+
type LayerAllocation,
|
|
84
|
+
type LayerCandidate,
|
|
85
|
+
} from './token-budget.js';
|
|
86
|
+
import {
|
|
87
|
+
adaptTokenCounter,
|
|
88
|
+
type ContextTokenCounter,
|
|
89
|
+
countMessageTokens,
|
|
90
|
+
HEURISTIC_TOKEN_COUNTER,
|
|
91
|
+
} from './token-counter.js';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Per-layer cap configuration. Mirrors the documented
|
|
95
|
+
* `ContextEngineConfig.layers.*` fields.
|
|
96
|
+
*
|
|
97
|
+
* @stable
|
|
98
|
+
*/
|
|
99
|
+
export interface LayerConfig {
|
|
100
|
+
readonly enabled?: boolean;
|
|
101
|
+
readonly cap?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Auto-recall config knob. `false` disables; `{ topK }` enables
|
|
106
|
+
* the heuristic with a bounded top-K.
|
|
107
|
+
*
|
|
108
|
+
* @stable
|
|
109
|
+
*/
|
|
110
|
+
export type AutoRecallConfig =
|
|
111
|
+
| false
|
|
112
|
+
| {
|
|
113
|
+
readonly topK?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Minimum fused score a hit must reach to be injected. **Default `0`**
|
|
116
|
+
* (CE-4) - `topK` already bounds the volume. The scale is
|
|
117
|
+
* reranker-dependent: the default RRF reranker fuses the FTS + vector
|
|
118
|
+
* candidate lists as `1/(60 + rank)` per list, so scores top out near
|
|
119
|
+
* `2/(60 + 1) ≈ 0.033` - any positive default would silently drop every
|
|
120
|
+
* hit. Set this only when calibrating against a known reranker's scale.
|
|
121
|
+
*/
|
|
122
|
+
readonly threshold?: number;
|
|
123
|
+
readonly strategy?: AutoRecallStrategy;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Privacy block of the engine config.
|
|
128
|
+
*
|
|
129
|
+
* @stable
|
|
130
|
+
*/
|
|
131
|
+
export interface PrivacyConfig {
|
|
132
|
+
readonly cloudUploadConsent?: boolean;
|
|
133
|
+
readonly defaultSensitivity?: Sensitivity;
|
|
134
|
+
readonly providerAcceptsSensitivity?: ReadonlyArray<Sensitivity>;
|
|
135
|
+
readonly providerTrust?: LocalProviderTrust;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Configuration accepted by {@link createContextEngine}.
|
|
140
|
+
*
|
|
141
|
+
* @stable
|
|
142
|
+
*/
|
|
143
|
+
export interface ContextEngineConfig {
|
|
144
|
+
/**
|
|
145
|
+
* Layer 1 base-template mode. `'full'` (default) ships the
|
|
146
|
+
* verbose ~250-350 token narrative aimed at general LLMs;
|
|
147
|
+
* `'minimal'` opts top-tier models into the ~80-120 token
|
|
148
|
+
* compact form.
|
|
149
|
+
*/
|
|
150
|
+
readonly memoryBaseMode?: MemoryBaseMode;
|
|
151
|
+
/** Default `'en'`. Pluggable via `defineContextLocalePack`. */
|
|
152
|
+
readonly locale?: string | ContextLocalePack | PartialContextLocalePack;
|
|
153
|
+
/** Per-layer enable / cap overrides. */
|
|
154
|
+
readonly layers?: {
|
|
155
|
+
readonly identity?: LayerConfig;
|
|
156
|
+
readonly memoryMetadata?: LayerConfig;
|
|
157
|
+
readonly activeRules?: LayerConfig;
|
|
158
|
+
readonly activeSkills?: LayerConfig;
|
|
159
|
+
readonly workingBlocks?: LayerConfig;
|
|
160
|
+
readonly autoRecall?: LayerConfig & { readonly topK?: number; readonly threshold?: number };
|
|
161
|
+
};
|
|
162
|
+
/** Auto-recall trigger configuration. Default `false`. */
|
|
163
|
+
readonly factsAutoRecall?: AutoRecallConfig;
|
|
164
|
+
/** Privacy-filter configuration. */
|
|
165
|
+
readonly privacy?: PrivacyConfig;
|
|
166
|
+
/** Hard token budget. Default `Number.POSITIVE_INFINITY` (no global cap). */
|
|
167
|
+
readonly maxContextTokens?: number;
|
|
168
|
+
/** Tokens reserved for the model's response. Default `4096`. */
|
|
169
|
+
readonly reservedForResponse?: number;
|
|
170
|
+
/** Tokens reserved for the compaction summarizer call. Default `8192`. */
|
|
171
|
+
readonly reservedForCompaction?: number;
|
|
172
|
+
/** Pluggable token counter. Default heuristic (chars/4). */
|
|
173
|
+
readonly tokenCounter?: TokenCounter | ContextTokenCounter;
|
|
174
|
+
/** Auto-compaction configuration (RB-46). */
|
|
175
|
+
readonly compaction?: false | CompactionConfig;
|
|
176
|
+
/** Active provider's context window; required when compaction is enabled. */
|
|
177
|
+
readonly providerContextWindow?: number;
|
|
178
|
+
/** Default summarizer adapter the auto-trigger uses. */
|
|
179
|
+
readonly summarizer?: CompactionSummarizer;
|
|
180
|
+
/** Wall clock for tests + deterministic compaction. */
|
|
181
|
+
readonly now?: () => number;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The IO shapes (AssembleInput / AnnotatedPart / AssembledPrompt /
|
|
185
|
+
// ResolvedContextEngineConfig) live in the io-types leaf and the
|
|
186
|
+
// ContextEngine interface itself is co-located with `Memory` in
|
|
187
|
+
// memory-interface.ts (issue #22 - the pair is mutually recursive).
|
|
188
|
+
// Both are re-exported here so every existing import path keeps
|
|
189
|
+
// working unchanged.
|
|
190
|
+
export type { ContextEngine } from '../memory-interface.js';
|
|
191
|
+
export type {
|
|
192
|
+
AnnotatedPart,
|
|
193
|
+
AssembledPrompt,
|
|
194
|
+
AssembleInput,
|
|
195
|
+
ResolvedContextEngineConfig,
|
|
196
|
+
} from './io-types.js';
|
|
197
|
+
|
|
198
|
+
const DEFAULT_LAYER_CAPS: Readonly<
|
|
199
|
+
Record<keyof Required<NonNullable<ContextEngineConfig['layers']>>, number | undefined>
|
|
200
|
+
> = {
|
|
201
|
+
identity: undefined,
|
|
202
|
+
memoryMetadata: undefined,
|
|
203
|
+
activeRules: undefined,
|
|
204
|
+
activeSkills: undefined,
|
|
205
|
+
workingBlocks: undefined,
|
|
206
|
+
autoRecall: undefined,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
let compactionIneffectiveWarned = false;
|
|
210
|
+
let heuristicCounterWarned = false;
|
|
211
|
+
|
|
212
|
+
/** Emit the "compaction enabled but ineffective" warning at most once (CE-12). */
|
|
213
|
+
function warnCompactionIneffective(message: string): void {
|
|
214
|
+
if (compactionIneffectiveWarned) return;
|
|
215
|
+
compactionIneffectiveWarned = true;
|
|
216
|
+
process.stderr.write(`[graphorin/memory] ${message}\n`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Test-only - reset the one-time CE-12 compaction warning so a test can assert
|
|
221
|
+
* it fires.
|
|
222
|
+
*
|
|
223
|
+
* @internal
|
|
224
|
+
*/
|
|
225
|
+
export function _resetCompactionWarningForTesting(): void {
|
|
226
|
+
compactionIneffectiveWarned = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @internal - test seam for the one-time heuristic-counter warning (CE-13). */
|
|
230
|
+
export function _resetHeuristicCounterWarningForTesting(): void {
|
|
231
|
+
heuristicCounterWarned = false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Emission order for the assembled layers - deliberately **distinct from the
|
|
236
|
+
* truncation priority ladder** (CE-9). Allocation still trims by
|
|
237
|
+
* `DEFAULT_LAYER_PRIORITY` (lowest priority first), but layers are *emitted* in
|
|
238
|
+
* this order so the volatile blocks that change every turn (`memoryMetadata`'s
|
|
239
|
+
* counts, `autoRecall`'s injected facts) sit **after** the stable Layer 1-4
|
|
240
|
+
* prefix (identity / rules / blocks / skills). That keeps the provider's - and a
|
|
241
|
+
* local llama.cpp / vLLM server's - KV-cache breakpoint real: the prefix stays
|
|
242
|
+
* byte-identical across turns even as the message count and recalled facts move.
|
|
243
|
+
*/
|
|
244
|
+
const LAYER_EMIT_ORDER: Readonly<Record<LayerAllocation['id'], number>> = Object.freeze({
|
|
245
|
+
identity: 0,
|
|
246
|
+
activeRules: 1,
|
|
247
|
+
workingBlocks: 2,
|
|
248
|
+
activeSkills: 3,
|
|
249
|
+
// --- KV-cache breakpoint: everything below changes per turn ---
|
|
250
|
+
memoryMetadata: 4,
|
|
251
|
+
autoRecall: 5,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build a ContextEngine instance from the supplied configuration.
|
|
256
|
+
*
|
|
257
|
+
* @stable
|
|
258
|
+
*/
|
|
259
|
+
export function createContextEngine(config: ContextEngineConfig = {}): ContextEngine {
|
|
260
|
+
const memoryBaseMode = config.memoryBaseMode ?? 'full';
|
|
261
|
+
const localeInput = config.locale;
|
|
262
|
+
const localePack: ContextLocalePack = resolvePackInput(localeInput);
|
|
263
|
+
const localeId = localePack.id;
|
|
264
|
+
const layerCfg = config.layers ?? {};
|
|
265
|
+
const layersEnabled: Record<string, boolean> = {
|
|
266
|
+
identity: layerCfg.identity?.enabled ?? true,
|
|
267
|
+
memoryMetadata: layerCfg.memoryMetadata?.enabled ?? true,
|
|
268
|
+
activeRules: layerCfg.activeRules?.enabled ?? true,
|
|
269
|
+
activeSkills: layerCfg.activeSkills?.enabled ?? true,
|
|
270
|
+
workingBlocks: layerCfg.workingBlocks?.enabled ?? true,
|
|
271
|
+
autoRecall: layerCfg.autoRecall?.enabled ?? true,
|
|
272
|
+
};
|
|
273
|
+
const layerCaps: Record<string, number | undefined> = {
|
|
274
|
+
identity: layerCfg.identity?.cap ?? DEFAULT_LAYER_CAPS.identity,
|
|
275
|
+
memoryMetadata: layerCfg.memoryMetadata?.cap ?? DEFAULT_LAYER_CAPS.memoryMetadata,
|
|
276
|
+
activeRules: layerCfg.activeRules?.cap ?? DEFAULT_LAYER_CAPS.activeRules,
|
|
277
|
+
activeSkills: layerCfg.activeSkills?.cap ?? DEFAULT_LAYER_CAPS.activeSkills,
|
|
278
|
+
workingBlocks: layerCfg.workingBlocks?.cap ?? DEFAULT_LAYER_CAPS.workingBlocks,
|
|
279
|
+
autoRecall: layerCfg.autoRecall?.cap ?? DEFAULT_LAYER_CAPS.autoRecall,
|
|
280
|
+
};
|
|
281
|
+
const factsAutoRecall = config.factsAutoRecall ?? false;
|
|
282
|
+
const reservedForResponse = config.reservedForResponse ?? 4096;
|
|
283
|
+
const reservedForCompaction = config.reservedForCompaction ?? 8192;
|
|
284
|
+
const maxContextTokens = config.maxContextTokens ?? Number.POSITIVE_INFINITY;
|
|
285
|
+
|
|
286
|
+
const tokenCounter: ContextTokenCounter = config.tokenCounter
|
|
287
|
+
? 'count' in config.tokenCounter
|
|
288
|
+
? adaptTokenCounter(config.tokenCounter)
|
|
289
|
+
: (config.tokenCounter as ContextTokenCounter)
|
|
290
|
+
: HEURISTIC_TOKEN_COUNTER;
|
|
291
|
+
// CE-13: budgeting against a real provider window with the heuristic
|
|
292
|
+
// counter is approximate - say so once instead of failing silently
|
|
293
|
+
// late (the heuristic's failure mode is a provider context-length
|
|
294
|
+
// error before compaction fires).
|
|
295
|
+
if (
|
|
296
|
+
config.tokenCounter === undefined &&
|
|
297
|
+
typeof config.providerContextWindow === 'number' &&
|
|
298
|
+
!heuristicCounterWarned
|
|
299
|
+
) {
|
|
300
|
+
heuristicCounterWarned = true;
|
|
301
|
+
process.stderr.write(
|
|
302
|
+
'[graphorin/memory] context-engine token counts use the built-in heuristic (chars/4 + dense-script). For production budgeting against a real provider window, pass `contextEngine.tokenCounter` (e.g. the provider package JsTiktokenCounter).\n',
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const privacy = config.privacy ?? {};
|
|
307
|
+
const defaultSensitivity: Sensitivity = privacy.defaultSensitivity ?? 'internal';
|
|
308
|
+
const providerTrust: LocalProviderTrust = privacy.providerTrust ?? 'public-tls';
|
|
309
|
+
const cloudUploadConsent = privacy.cloudUploadConsent ?? false;
|
|
310
|
+
const heuristicStrategy = defaultLocaleHeuristicStrategy(localePack);
|
|
311
|
+
|
|
312
|
+
// Compaction config resolution.
|
|
313
|
+
const compactionInput = config.compaction;
|
|
314
|
+
const compactionAutoDefault = resolveAutoCompactionDefault(providerTrust);
|
|
315
|
+
const compactionEnabled =
|
|
316
|
+
compactionInput === false
|
|
317
|
+
? false
|
|
318
|
+
: compactionInput === undefined
|
|
319
|
+
? compactionAutoDefault === 'enabled'
|
|
320
|
+
: compactionInput.trigger !== 'never';
|
|
321
|
+
const providerContextWindow = config.providerContextWindow ?? null;
|
|
322
|
+
const triggerSpec =
|
|
323
|
+
compactionInput === false || compactionInput === undefined
|
|
324
|
+
? undefined
|
|
325
|
+
: compactionInput.trigger;
|
|
326
|
+
const compactionThresholdTokens =
|
|
327
|
+
compactionEnabled && providerContextWindow !== null
|
|
328
|
+
? resolveTriggerThreshold({
|
|
329
|
+
contextWindow: providerContextWindow,
|
|
330
|
+
...(triggerSpec !== undefined ? { trigger: triggerSpec } : {}),
|
|
331
|
+
reservedForResponse,
|
|
332
|
+
reservedForCompaction,
|
|
333
|
+
})
|
|
334
|
+
: Number.POSITIVE_INFINITY;
|
|
335
|
+
// CE-12: compaction enabled without a `providerContextWindow` leaves the
|
|
336
|
+
// threshold at Infinity, so `shouldCompact` returns false forever - a
|
|
337
|
+
// silently-dead default-on protection. Surface it: throw if the operator
|
|
338
|
+
// explicitly configured compaction, warn (once) if it is on by the default
|
|
339
|
+
// trust policy. Auto-detecting the window from the provider is not
|
|
340
|
+
// implemented.
|
|
341
|
+
const compactionEffective = compactionEnabled && providerContextWindow !== null;
|
|
342
|
+
if (compactionEnabled && providerContextWindow === null) {
|
|
343
|
+
const message =
|
|
344
|
+
'context-engine compaction is enabled but `providerContextWindow` is not set, so the ' +
|
|
345
|
+
'trigger threshold is Infinity and compaction will never fire. Pass `providerContextWindow` ' +
|
|
346
|
+
"(your model's context window, in tokens) - auto-detection from the provider is not implemented.";
|
|
347
|
+
if (compactionInput !== undefined && compactionInput !== false) {
|
|
348
|
+
throw new Error(`[graphorin/memory] ${message}`);
|
|
349
|
+
}
|
|
350
|
+
warnCompactionIneffective(message);
|
|
351
|
+
}
|
|
352
|
+
const compactionStrategy: CompactionStrategy =
|
|
353
|
+
compactionInput === false ||
|
|
354
|
+
compactionInput === undefined ||
|
|
355
|
+
compactionInput.strategy === undefined
|
|
356
|
+
? {
|
|
357
|
+
kind: 'summarize-old-preserve-recent',
|
|
358
|
+
preserveRecentTurns: 6,
|
|
359
|
+
templateName: 'summary-sections',
|
|
360
|
+
}
|
|
361
|
+
: compactionInput.strategy;
|
|
362
|
+
// SOTA-4 reclaim-floor: defer a compaction whose older (compactable) portion
|
|
363
|
+
// is below this floor (read from the trigger config; 0 = off, byte-identical).
|
|
364
|
+
const compactionMinReclaimTokens =
|
|
365
|
+
triggerSpec !== undefined && triggerSpec !== 'never'
|
|
366
|
+
? Math.max(0, triggerSpec.minReclaimTokens ?? 0)
|
|
367
|
+
: 0;
|
|
368
|
+
// How many recent turns the active strategy preserves (the reclaimable portion
|
|
369
|
+
// is everything before them).
|
|
370
|
+
const reclaimPreserveTurns =
|
|
371
|
+
compactionStrategy.kind === 'summarize-old-preserve-recent'
|
|
372
|
+
? (compactionStrategy.preserveRecentTurns ?? DEFAULT_PRESERVE_RECENT_TURNS)
|
|
373
|
+
: compactionStrategy.kind === 'clear-old-tool-results' &&
|
|
374
|
+
typeof compactionStrategy.summarizeFallback === 'object'
|
|
375
|
+
? (compactionStrategy.summarizeFallback.preserveRecentTurns ??
|
|
376
|
+
DEFAULT_PRESERVE_RECENT_TURNS)
|
|
377
|
+
: DEFAULT_PRESERVE_RECENT_TURNS;
|
|
378
|
+
const compactionHooks: ReadonlyArray<NamedPostCompactionHook> = resolveDefaultHooks(
|
|
379
|
+
compactionInput === false || compactionInput === undefined
|
|
380
|
+
? undefined
|
|
381
|
+
: compactionInput.postCompactionHooks,
|
|
382
|
+
);
|
|
383
|
+
const summarizer = config.summarizer;
|
|
384
|
+
const now = config.now ?? Date.now;
|
|
385
|
+
|
|
386
|
+
const resolvedConfig: ResolvedContextEngineConfig = Object.freeze({
|
|
387
|
+
memoryBaseMode,
|
|
388
|
+
localeId,
|
|
389
|
+
maxContextTokens,
|
|
390
|
+
reservedForResponse,
|
|
391
|
+
reservedForCompaction,
|
|
392
|
+
compactionEnabled,
|
|
393
|
+
compactionEffective,
|
|
394
|
+
compactionThresholdTokens,
|
|
395
|
+
providerContextWindow,
|
|
396
|
+
providerTrust,
|
|
397
|
+
cloudUploadConsent,
|
|
398
|
+
defaultSensitivity,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
async function assemble(memory: Memory, input: AssembleInput): Promise<AssembledPrompt> {
|
|
402
|
+
// Layer 1 - base template.
|
|
403
|
+
const layer1Text = layersEnabled.identity ? composeLayer1(localePack, memoryBaseMode) : '';
|
|
404
|
+
// Layer 2 - agent instructions.
|
|
405
|
+
const layer2Text = composeLayer2(input.agentInstructions);
|
|
406
|
+
|
|
407
|
+
// Layer 3 - working blocks (filter D2).
|
|
408
|
+
const blocks = await memory.working.list(input.scope);
|
|
409
|
+
const blocksKept: Array<(typeof blocks)[number]> = [];
|
|
410
|
+
const privacyCounters: Record<PrivacyDecisionReason, number> = {
|
|
411
|
+
allowed: 0,
|
|
412
|
+
'no-cloud-upload-consent': 0,
|
|
413
|
+
'provider-rejects-internal': 0,
|
|
414
|
+
'provider-rejects-secret': 0,
|
|
415
|
+
};
|
|
416
|
+
for (const block of blocks) {
|
|
417
|
+
const decision = privacyDecide(block.sensitivity, {
|
|
418
|
+
...(privacy.providerAcceptsSensitivity !== undefined
|
|
419
|
+
? { providerAcceptsSensitivity: privacy.providerAcceptsSensitivity }
|
|
420
|
+
: {}),
|
|
421
|
+
providerTrust,
|
|
422
|
+
cloudUploadConsent,
|
|
423
|
+
defaultSensitivity,
|
|
424
|
+
});
|
|
425
|
+
privacyCounters[decision.reason] += 1;
|
|
426
|
+
if (decision.decision === 'pass') blocksKept.push(block);
|
|
427
|
+
}
|
|
428
|
+
const layer3Text = layersEnabled.workingBlocks ? renderWorkingBlocks(blocksKept) : '';
|
|
429
|
+
|
|
430
|
+
// Layer 4 - procedural rules + skills (filter D2 on rules).
|
|
431
|
+
const rules = await memory.procedural.activate(input.scope, input.proceduralActivation ?? {});
|
|
432
|
+
const rulesKept: Array<(typeof rules)[number]> = [];
|
|
433
|
+
for (const rule of rules) {
|
|
434
|
+
const decision = privacyDecide(rule.sensitivity, {
|
|
435
|
+
...(privacy.providerAcceptsSensitivity !== undefined
|
|
436
|
+
? { providerAcceptsSensitivity: privacy.providerAcceptsSensitivity }
|
|
437
|
+
: {}),
|
|
438
|
+
providerTrust,
|
|
439
|
+
cloudUploadConsent,
|
|
440
|
+
defaultSensitivity,
|
|
441
|
+
});
|
|
442
|
+
privacyCounters[decision.reason] += 1;
|
|
443
|
+
if (decision.decision === 'pass') rulesKept.push(rule);
|
|
444
|
+
}
|
|
445
|
+
const layer4RulesText = layersEnabled.activeRules ? renderProceduralRules(rulesKept) : '';
|
|
446
|
+
const layer4SkillsText = layersEnabled.activeSkills
|
|
447
|
+
? composeLayer4Skills(input.skills ?? [])
|
|
448
|
+
: '';
|
|
449
|
+
|
|
450
|
+
// Layer 5 - memory metadata.
|
|
451
|
+
const meta = await memory.metadata(input.scope);
|
|
452
|
+
const layer5Text = layersEnabled.memoryMetadata
|
|
453
|
+
? renderMetadataBlock(enrichMetadataTags(meta, localeId, memory.embedderId()))
|
|
454
|
+
: '';
|
|
455
|
+
|
|
456
|
+
// Layer 6 - auto-recall.
|
|
457
|
+
let autoRecall: AutoRecallTriggerResult = { factsTriggered: false };
|
|
458
|
+
let layer6Text = '';
|
|
459
|
+
if (factsAutoRecall !== false && layersEnabled.autoRecall) {
|
|
460
|
+
const strategy =
|
|
461
|
+
input.autoRecallStrategyOverride ?? factsAutoRecall.strategy ?? heuristicStrategy;
|
|
462
|
+
autoRecall = strategy({
|
|
463
|
+
locale: localeId,
|
|
464
|
+
lastUserMessage: input.lastUserMessage ?? '',
|
|
465
|
+
});
|
|
466
|
+
if (autoRecall.factsTriggered) {
|
|
467
|
+
const topK = factsAutoRecall.topK ?? layerCfg.autoRecall?.topK ?? 5;
|
|
468
|
+
// CE-4: default 0 - rank-based `topK` already bounds the injected
|
|
469
|
+
// volume, and the threshold's scale is reranker-dependent (the default
|
|
470
|
+
// RRF reranker fuses 2 candidate lists, so scores top out near
|
|
471
|
+
// `2/(60+1) ≈ 0.033` and any positive default would silently drop
|
|
472
|
+
// everything). Operators tuning against a known reranker scale can pass
|
|
473
|
+
// an explicit `threshold`.
|
|
474
|
+
const threshold = factsAutoRecall.threshold ?? layerCfg.autoRecall?.threshold ?? 0;
|
|
475
|
+
const hits = await memory.semantic
|
|
476
|
+
.search(input.scope, input.lastUserMessage ?? '', { topK })
|
|
477
|
+
.catch(() => []);
|
|
478
|
+
const facts: Fact[] = [];
|
|
479
|
+
for (const hit of hits) {
|
|
480
|
+
if (hit.score < threshold) continue;
|
|
481
|
+
const decision = privacyDecide(hit.record.sensitivity, {
|
|
482
|
+
...(privacy.providerAcceptsSensitivity !== undefined
|
|
483
|
+
? { providerAcceptsSensitivity: privacy.providerAcceptsSensitivity }
|
|
484
|
+
: {}),
|
|
485
|
+
providerTrust,
|
|
486
|
+
cloudUploadConsent,
|
|
487
|
+
defaultSensitivity,
|
|
488
|
+
});
|
|
489
|
+
privacyCounters[decision.reason] += 1;
|
|
490
|
+
if (decision.decision === 'pass') facts.push(hit.record);
|
|
491
|
+
}
|
|
492
|
+
layer6Text = renderAutoRecalledFacts(facts);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Token-budget allocation. Layer 1 (`graphorin_memory_base`) and
|
|
497
|
+
// Layer 2 (`agent_instructions`) are concatenated into the
|
|
498
|
+
// single `identity` candidate so they share a priority slot;
|
|
499
|
+
// they are joined with a blank line so the assembled prompt is
|
|
500
|
+
// readable.
|
|
501
|
+
const identityText =
|
|
502
|
+
layer1Text.length > 0 && layer2Text.length > 0
|
|
503
|
+
? `${layer1Text}\n\n${layer2Text}`
|
|
504
|
+
: layer1Text + layer2Text;
|
|
505
|
+
const candidates: LayerCandidate[] = [
|
|
506
|
+
buildCandidate('identity', identityText, layerCaps.identity),
|
|
507
|
+
buildCandidate('memoryMetadata', layer5Text, layerCaps.memoryMetadata),
|
|
508
|
+
buildCandidate('activeRules', layer4RulesText, layerCaps.activeRules),
|
|
509
|
+
buildCandidate('workingBlocks', layer3Text, layerCaps.workingBlocks),
|
|
510
|
+
buildCandidate('activeSkills', layer4SkillsText, layerCaps.activeSkills),
|
|
511
|
+
buildCandidate('autoRecall', layer6Text, layerCaps.autoRecall),
|
|
512
|
+
].filter((c) => c.text.length > 0);
|
|
513
|
+
|
|
514
|
+
const allocation = await allocateLayers(candidates, maxContextTokens, tokenCounter);
|
|
515
|
+
|
|
516
|
+
// D4 inbound preamble - fires on steps containing untrusted upstream parts.
|
|
517
|
+
const preambleFired = shouldFireInboundPreamble(input.upstreamAnnotations ?? []);
|
|
518
|
+
const preambleText = preambleFired ? composeInboundPreamble(localePack) : '';
|
|
519
|
+
|
|
520
|
+
// CE-9: emit in stability order (Layer 1-4 prefix, then the cache
|
|
521
|
+
// breakpoint, then the volatile metadata / auto-recall), NOT in the
|
|
522
|
+
// truncation-priority order `allocation.layers` arrives in.
|
|
523
|
+
const assembledLayers = allocation.layers
|
|
524
|
+
.filter((l) => l.text.length > 0)
|
|
525
|
+
.sort((a, b) => LAYER_EMIT_ORDER[a.id] - LAYER_EMIT_ORDER[b.id]);
|
|
526
|
+
const finalParts: string[] = assembledLayers.map((l) => l.text);
|
|
527
|
+
if (preambleText.length > 0) finalParts.push(preambleText);
|
|
528
|
+
const systemContent = finalParts.join('\n\n');
|
|
529
|
+
|
|
530
|
+
// Annotations - one per assembled fragment, in the same order
|
|
531
|
+
// as `finalParts` so observers can correlate.
|
|
532
|
+
const annotations: AnnotatedPart[] = assembledLayers.map((layer) => ({
|
|
533
|
+
content: { type: 'text', text: layer.text } as MessageContent,
|
|
534
|
+
annotation: annotationForLayer(layer),
|
|
535
|
+
}));
|
|
536
|
+
if (preambleText.length > 0) {
|
|
537
|
+
annotations.push({
|
|
538
|
+
content: { type: 'text', text: preambleText } as MessageContent,
|
|
539
|
+
annotation: annotate('system:framework', 'n/a'),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
systemMessage: Object.freeze({ role: 'system' as const, content: systemContent }),
|
|
545
|
+
annotations: Object.freeze(annotations),
|
|
546
|
+
layerAllocation: allocation,
|
|
547
|
+
inboundPreambleFired: preambleFired,
|
|
548
|
+
privacyCounters: Object.freeze(privacyCounters),
|
|
549
|
+
localeId,
|
|
550
|
+
memoryBaseMode,
|
|
551
|
+
autoRecall,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// CE-7 anti-thrash: after a compaction whose afterTokens still sits at
|
|
556
|
+
// or above the trigger threshold (e.g. huge tool results among the
|
|
557
|
+
// preserved turns), an immediate re-trigger would summarize the
|
|
558
|
+
// previous summary - spend with no reclaim. Track the last outcome and
|
|
559
|
+
// require the buffer to have GROWN past it before firing again.
|
|
560
|
+
let lastCompactionAfterTokens: number | null = null;
|
|
561
|
+
const REARM_GROWTH_TOKENS = 256;
|
|
562
|
+
|
|
563
|
+
// C4 (context-engine-06/07 hardening): a persistently-failing summarizer
|
|
564
|
+
// previously re-fired an LLM call at the top of EVERY step (the
|
|
565
|
+
// anti-thrash guard arms only on success). Count consecutive failures -
|
|
566
|
+
// including "successful" passes that did not actually shrink the buffer
|
|
567
|
+
// - retry each pass once with a short backoff, and after
|
|
568
|
+
// MAX_CONSECUTIVE_COMPACTION_FAILURES disable the AUTO trigger until a
|
|
569
|
+
// later compactNow (e.g. a manual compact with a fixed summarizer)
|
|
570
|
+
// succeeds and re-arms it.
|
|
571
|
+
let consecutiveCompactionFailures = 0;
|
|
572
|
+
let autoCompactionDisabledByFailures = false;
|
|
573
|
+
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
574
|
+
const COMPACTION_RETRY_BACKOFF_MS = 250;
|
|
575
|
+
|
|
576
|
+
function recordCompactionFailure(reason: string): void {
|
|
577
|
+
consecutiveCompactionFailures += 1;
|
|
578
|
+
if (
|
|
579
|
+
consecutiveCompactionFailures >= MAX_CONSECUTIVE_COMPACTION_FAILURES &&
|
|
580
|
+
!autoCompactionDisabledByFailures
|
|
581
|
+
) {
|
|
582
|
+
autoCompactionDisabledByFailures = true;
|
|
583
|
+
process.stderr.write(
|
|
584
|
+
`[graphorin/memory] auto-compaction disabled after ${consecutiveCompactionFailures} consecutive failed passes (last: ${reason}). Manual compactNow() still runs and re-enables the trigger on success.\n`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function shouldCompact(
|
|
590
|
+
messages: ReadonlyArray<Message>,
|
|
591
|
+
options: {
|
|
592
|
+
readonly precomputedTokens?: number;
|
|
593
|
+
readonly compactableFromIndex?: number;
|
|
594
|
+
} = {},
|
|
595
|
+
): Promise<boolean> {
|
|
596
|
+
if (!compactionEnabled) return false;
|
|
597
|
+
if (autoCompactionDisabledByFailures) return false;
|
|
598
|
+
if (compactionThresholdTokens === Number.POSITIVE_INFINITY) return false;
|
|
599
|
+
const total = options.precomputedTokens ?? (await countMessageTokens(messages, tokenCounter));
|
|
600
|
+
if (total < compactionThresholdTokens) return false;
|
|
601
|
+
if (
|
|
602
|
+
lastCompactionAfterTokens !== null &&
|
|
603
|
+
total <= lastCompactionAfterTokens + REARM_GROWTH_TOKENS
|
|
604
|
+
) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
// SOTA-4 reclaim-floor: skip when the older (compactable) portion is too
|
|
608
|
+
// small to be worth a summarizer call - avoids compact-thrash at the
|
|
609
|
+
// threshold. Counts only the older slice; cheap relative to the avoided
|
|
610
|
+
// call. context-engine-04: the caller's pinned system prefix
|
|
611
|
+
// (`compactableFromIndex`) is NOT reclaimable and must not be counted -
|
|
612
|
+
// a 10k-token prefix would otherwise satisfy any floor while the body
|
|
613
|
+
// holds near-zero reclaimable tokens.
|
|
614
|
+
if (compactionMinReclaimTokens > 0) {
|
|
615
|
+
const from = Math.max(0, Math.min(options.compactableFromIndex ?? 0, messages.length));
|
|
616
|
+
const olderCount = Math.max(from, messages.length - reclaimPreserveTurns);
|
|
617
|
+
if (olderCount <= from) return false;
|
|
618
|
+
const olderTokens = await countMessageTokens(messages.slice(from, olderCount), tokenCounter);
|
|
619
|
+
if (olderTokens < compactionMinReclaimTokens) return false;
|
|
620
|
+
}
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function compactNow(callInput: {
|
|
625
|
+
readonly scope: SessionScope;
|
|
626
|
+
readonly runId: string;
|
|
627
|
+
readonly sessionId: string;
|
|
628
|
+
readonly agentId: string;
|
|
629
|
+
readonly source: CompactionSource;
|
|
630
|
+
readonly messages: ReadonlyArray<Message>;
|
|
631
|
+
readonly memory: Memory;
|
|
632
|
+
readonly summarizer?: CompactionSummarizer;
|
|
633
|
+
/** Per-call override of the strategy's preserve-recent count (CE-3). */
|
|
634
|
+
readonly preserveRecentTurns?: number;
|
|
635
|
+
/** Topic/tags narrowing for the procedural-rules re-anchor hook (CE-6). */
|
|
636
|
+
readonly procedural?: { readonly topic?: string; readonly tags?: ReadonlyArray<string> };
|
|
637
|
+
/** The caller's pinned system prefix - accounting only (context-engine-04). */
|
|
638
|
+
readonly prefixMessages?: ReadonlyArray<Message>;
|
|
639
|
+
readonly signal?: AbortSignal;
|
|
640
|
+
}): Promise<{
|
|
641
|
+
readonly result: CompactionResult;
|
|
642
|
+
readonly extraContent: ReadonlyArray<MessageContent>;
|
|
643
|
+
readonly hookFailures: ReadonlyArray<{ readonly hookName: string; readonly reason: string }>;
|
|
644
|
+
}> {
|
|
645
|
+
const activeSummarizer = callInput.summarizer ?? summarizer;
|
|
646
|
+
if (activeSummarizer === undefined) {
|
|
647
|
+
throw new TypeError(
|
|
648
|
+
'[graphorin/memory] ContextEngine.compactNow: no summarizer configured. ' +
|
|
649
|
+
'Pass `summarizer` to createContextEngine({...}) or to compactNow({...}).',
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const compactionInputCall: ExecuteCompactionInput = {
|
|
653
|
+
messages: callInput.messages,
|
|
654
|
+
source: callInput.source,
|
|
655
|
+
strategy:
|
|
656
|
+
callInput.preserveRecentTurns !== undefined &&
|
|
657
|
+
compactionStrategy.kind === 'summarize-old-preserve-recent'
|
|
658
|
+
? { ...compactionStrategy, preserveRecentTurns: callInput.preserveRecentTurns }
|
|
659
|
+
: compactionStrategy,
|
|
660
|
+
localePack,
|
|
661
|
+
summarizer: activeSummarizer,
|
|
662
|
+
tokenCounter,
|
|
663
|
+
thresholdTokens: compactionThresholdTokens,
|
|
664
|
+
runId: callInput.runId,
|
|
665
|
+
sessionId: callInput.sessionId,
|
|
666
|
+
agentId: callInput.agentId,
|
|
667
|
+
scope: callInput.scope,
|
|
668
|
+
providerTrust,
|
|
669
|
+
now,
|
|
670
|
+
...(callInput.signal !== undefined ? { signal: callInput.signal } : {}),
|
|
671
|
+
};
|
|
672
|
+
// C4: one retry with a short backoff on summarizer failure, then a
|
|
673
|
+
// strictly-smaller-than-input assert - a pass that dropped messages
|
|
674
|
+
// yet did not shrink the buffer is a failure (a looping/echoing
|
|
675
|
+
// summarizer), not a success to arm the anti-thrash guard with.
|
|
676
|
+
let result: CompactionResult;
|
|
677
|
+
try {
|
|
678
|
+
result = await executeCompaction(compactionInputCall);
|
|
679
|
+
} catch (firstError) {
|
|
680
|
+
if (callInput.signal?.aborted === true) throw firstError;
|
|
681
|
+
await new Promise<void>((resolve) => setTimeout(resolve, COMPACTION_RETRY_BACKOFF_MS));
|
|
682
|
+
try {
|
|
683
|
+
result = await executeCompaction(compactionInputCall);
|
|
684
|
+
} catch (secondError) {
|
|
685
|
+
recordCompactionFailure(
|
|
686
|
+
secondError instanceof Error ? secondError.message : String(secondError),
|
|
687
|
+
);
|
|
688
|
+
throw secondError;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (result.droppedMessageIndices.length > 0 && result.afterTokens >= result.beforeTokens) {
|
|
692
|
+
const reason = `compaction did not shrink the buffer (${result.beforeTokens} -> ${result.afterTokens} tokens)`;
|
|
693
|
+
if (callInput.source === 'auto-trigger') {
|
|
694
|
+
// The auto loop must never keep paying for passes that grow the
|
|
695
|
+
// buffer (the Gemini-CLI compression-loop class) - fail the pass.
|
|
696
|
+
recordCompactionFailure(reason);
|
|
697
|
+
throw new Error(`[graphorin/memory] ${reason}`);
|
|
698
|
+
}
|
|
699
|
+
// An operator-requested compaction of a small window can
|
|
700
|
+
// legitimately cost more than it reclaims (the summary skeleton
|
|
701
|
+
// has a floor) - warn, hand back the result, count nothing.
|
|
702
|
+
process.stderr.write(`[graphorin/memory] ${reason} (source: ${callInput.source}).\n`);
|
|
703
|
+
} else if (result.droppedMessageIndices.length > 0) {
|
|
704
|
+
// A pass that actually reclaimed space re-arms the auto trigger.
|
|
705
|
+
consecutiveCompactionFailures = 0;
|
|
706
|
+
if (autoCompactionDisabledByFailures) {
|
|
707
|
+
autoCompactionDisabledByFailures = false;
|
|
708
|
+
process.stderr.write(
|
|
709
|
+
'[graphorin/memory] auto-compaction re-enabled after a successful pass.\n',
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// C4: hand hooks the DROPPED message content (not just indices) so
|
|
714
|
+
// re-anchoring hooks like reanchorRecentResults can recover the
|
|
715
|
+
// result handles that just left the window. Reference-diff against
|
|
716
|
+
// trimmedMessages (both strategies carry surviving originals by
|
|
717
|
+
// reference; the per-strategy index spaces differ, so indices are
|
|
718
|
+
// not usable here).
|
|
719
|
+
const survivors = new Set(result.trimmedMessages);
|
|
720
|
+
const droppedMessages = callInput.messages.filter((m) => !survivors.has(m));
|
|
721
|
+
const ctx = {
|
|
722
|
+
result,
|
|
723
|
+
scope: callInput.scope,
|
|
724
|
+
runId: callInput.runId,
|
|
725
|
+
sessionId: callInput.sessionId,
|
|
726
|
+
agentId: callInput.agentId,
|
|
727
|
+
source: callInput.source,
|
|
728
|
+
droppedMessages,
|
|
729
|
+
};
|
|
730
|
+
const hookFailures: Array<{ readonly hookName: string; readonly reason: string }> = [];
|
|
731
|
+
const extraContent: MessageContent[] = [];
|
|
732
|
+
let hooksFired = 0;
|
|
733
|
+
// context-engine-02: the same D2 privacy decision `assemble()` applies
|
|
734
|
+
// to blocks / rules / facts, handed to the hooks - their output is
|
|
735
|
+
// spliced into the buffer and shipped to the provider, so it must obey
|
|
736
|
+
// the same filter or secret-tier content leaks on the first compaction.
|
|
737
|
+
const allowSensitivity = (sensitivity: Sensitivity | undefined): boolean =>
|
|
738
|
+
privacyDecide(sensitivity, {
|
|
739
|
+
...(privacy.providerAcceptsSensitivity !== undefined
|
|
740
|
+
? { providerAcceptsSensitivity: privacy.providerAcceptsSensitivity }
|
|
741
|
+
: {}),
|
|
742
|
+
providerTrust,
|
|
743
|
+
cloudUploadConsent,
|
|
744
|
+
defaultSensitivity,
|
|
745
|
+
}).decision === 'pass';
|
|
746
|
+
for (const hook of compactionHooks) {
|
|
747
|
+
try {
|
|
748
|
+
// CE-6: hooks receive the REAL compaction context - the old code
|
|
749
|
+
// built `ctx` and then `void ctx;`-discarded it while the
|
|
750
|
+
// function-form wrapper fabricated a zeroed result.
|
|
751
|
+
const parts = await hook.resolveContent(
|
|
752
|
+
{
|
|
753
|
+
memory: callInput.memory,
|
|
754
|
+
scope: callInput.scope,
|
|
755
|
+
...(callInput.procedural !== undefined ? { procedural: callInput.procedural } : {}),
|
|
756
|
+
allowSensitivity,
|
|
757
|
+
},
|
|
758
|
+
ctx,
|
|
759
|
+
);
|
|
760
|
+
extraContent.push(...parts);
|
|
761
|
+
hooksFired += 1;
|
|
762
|
+
} catch (err) {
|
|
763
|
+
hookFailures.push({
|
|
764
|
+
hookName: hook.id,
|
|
765
|
+
reason: err instanceof Error ? err.name : 'UnknownError',
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const enrichedResult: CompactionResult = Object.freeze({
|
|
770
|
+
...result,
|
|
771
|
+
hooksFiredCount: hooksFired,
|
|
772
|
+
});
|
|
773
|
+
// CE-7: arm the anti-thrash guard and surface the failure mode where
|
|
774
|
+
// compaction could not get under the trigger threshold.
|
|
775
|
+
//
|
|
776
|
+
// context-engine-04: `shouldCompact` compares the FULL buffer total
|
|
777
|
+
// (pinned prefix included) against this baseline, so the baseline must
|
|
778
|
+
// be full-basis too: body afterTokens + the caller's pinned prefix +
|
|
779
|
+
// the Context-Essentials message the caller splices in. Arming with
|
|
780
|
+
// the body-only number let any real system prompt (> 256 tokens)
|
|
781
|
+
// defeat the guard, re-firing a summarizer LLM call at the top of
|
|
782
|
+
// EVERY step whenever compaction could not get under the threshold.
|
|
783
|
+
const prefixTokens =
|
|
784
|
+
callInput.prefixMessages !== undefined && callInput.prefixMessages.length > 0
|
|
785
|
+
? await countMessageTokens(callInput.prefixMessages, tokenCounter)
|
|
786
|
+
: 0;
|
|
787
|
+
let essentialsTokens = 0;
|
|
788
|
+
for (const part of extraContent) {
|
|
789
|
+
if (part.type === 'text') essentialsTokens += await tokenCounter.countText(part.text);
|
|
790
|
+
}
|
|
791
|
+
const fullAfterTokens = enrichedResult.afterTokens + prefixTokens + essentialsTokens;
|
|
792
|
+
lastCompactionAfterTokens = fullAfterTokens;
|
|
793
|
+
if (
|
|
794
|
+
compactionThresholdTokens !== Number.POSITIVE_INFINITY &&
|
|
795
|
+
fullAfterTokens >= compactionThresholdTokens
|
|
796
|
+
) {
|
|
797
|
+
process.stderr.write(
|
|
798
|
+
`[graphorin/memory] compaction finished at ${fullAfterTokens} tokens (body ${enrichedResult.afterTokens} + pinned prefix ${prefixTokens} + essentials ${essentialsTokens}) - still at/above the ${compactionThresholdTokens}-token trigger (oversized preserved turns?). The immediate re-trigger is suppressed until the buffer grows.\n`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return Object.freeze({
|
|
802
|
+
result: enrichedResult,
|
|
803
|
+
extraContent: Object.freeze(extraContent),
|
|
804
|
+
hookFailures: Object.freeze(hookFailures),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return Object.freeze({
|
|
809
|
+
assemble,
|
|
810
|
+
shouldCompact,
|
|
811
|
+
compactNow,
|
|
812
|
+
config(): ResolvedContextEngineConfig {
|
|
813
|
+
return resolvedConfig;
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function buildCandidate(
|
|
819
|
+
id: LayerCandidate['id'],
|
|
820
|
+
text: string,
|
|
821
|
+
cap: number | undefined,
|
|
822
|
+
): LayerCandidate {
|
|
823
|
+
if (cap !== undefined) return { id, text, cap };
|
|
824
|
+
return { id, text };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function annotationForLayer(layer: LayerAllocation): ContentAnnotation {
|
|
828
|
+
switch (layer.id) {
|
|
829
|
+
case 'identity':
|
|
830
|
+
// Identity merges the framework base (Layer 1) + the user-defined
|
|
831
|
+
// agent_instructions (Layer 2) into one trusted fragment, tagged
|
|
832
|
+
// `system:framework` so D3 skips re-scanning it under
|
|
833
|
+
// `scanScope: 'untrusted'`. (Both halves are first-party prompt text, so
|
|
834
|
+
// the merged layer carries a single framework trust tag - `LayerAllocation`
|
|
835
|
+
// does not surface which halves were present.)
|
|
836
|
+
return annotate('system:framework', 'n/a');
|
|
837
|
+
case 'memoryMetadata':
|
|
838
|
+
case 'activeRules':
|
|
839
|
+
case 'workingBlocks':
|
|
840
|
+
case 'activeSkills':
|
|
841
|
+
case 'autoRecall':
|
|
842
|
+
return annotate('memory:tier-filtered', 'n/a');
|
|
843
|
+
default:
|
|
844
|
+
return annotate('system:framework', 'n/a');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function renderWorkingBlocks(
|
|
849
|
+
blocks: ReadonlyArray<{
|
|
850
|
+
readonly label: string;
|
|
851
|
+
readonly value: string;
|
|
852
|
+
readonly description?: string;
|
|
853
|
+
}>,
|
|
854
|
+
): string {
|
|
855
|
+
if (blocks.length === 0) return '';
|
|
856
|
+
const lines = ['<memory_blocks>'];
|
|
857
|
+
for (const block of blocks) {
|
|
858
|
+
const description =
|
|
859
|
+
block.description !== undefined ? ` description="${escapeXmlAttr(block.description)}"` : '';
|
|
860
|
+
lines.push(` <block label="${escapeXmlAttr(block.label)}"${description}>`);
|
|
861
|
+
lines.push(` ${escapeXmlText(block.value)}`);
|
|
862
|
+
lines.push(' </block>');
|
|
863
|
+
}
|
|
864
|
+
lines.push('</memory_blocks>');
|
|
865
|
+
return lines.join('\n');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function renderProceduralRules(
|
|
869
|
+
rules: ReadonlyArray<{ readonly text: string; readonly priority: number }>,
|
|
870
|
+
): string {
|
|
871
|
+
if (rules.length === 0) return '';
|
|
872
|
+
const lines = ['<memory_rules>'];
|
|
873
|
+
for (const rule of rules) {
|
|
874
|
+
lines.push(` <rule priority="${rule.priority}">${escapeXmlText(rule.text)}</rule>`);
|
|
875
|
+
}
|
|
876
|
+
lines.push('</memory_rules>');
|
|
877
|
+
return lines.join('\n');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function renderAutoRecalledFacts(facts: ReadonlyArray<Fact>): string {
|
|
881
|
+
if (facts.length === 0) return '';
|
|
882
|
+
const lines = ['<auto_recalled_facts>'];
|
|
883
|
+
for (const fact of facts) {
|
|
884
|
+
const tagsAttr =
|
|
885
|
+
fact.tags !== undefined ? ` tags="${escapeXmlAttr((fact.tags ?? []).join(','))}"` : '';
|
|
886
|
+
lines.push(
|
|
887
|
+
` <fact id="${escapeXmlAttr(fact.id)}"${tagsAttr}>${escapeXmlText(fact.text)}</fact>`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
lines.push('</auto_recalled_facts>');
|
|
891
|
+
return lines.join('\n');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function escapeXmlAttr(value: string): string {
|
|
895
|
+
return value
|
|
896
|
+
.replace(/&/g, '&')
|
|
897
|
+
.replace(/</g, '<')
|
|
898
|
+
.replace(/>/g, '>')
|
|
899
|
+
.replace(/"/g, '"');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function escapeXmlText(value: string): string {
|
|
903
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function resolvePackInput(input: ContextEngineConfig['locale']): ContextLocalePack {
|
|
907
|
+
if (input === undefined) return enLocalePack;
|
|
908
|
+
if (typeof input === 'string') {
|
|
909
|
+
if (input === enLocalePack.id) return enLocalePack;
|
|
910
|
+
return resolveLocalePack({ id: input });
|
|
911
|
+
}
|
|
912
|
+
return resolveLocalePack(input);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function resolveDefaultHooks(
|
|
916
|
+
input: ReadonlyArray<PostCompactionHook> | ReadonlyArray<NamedPostCompactionHook> | undefined,
|
|
917
|
+
): ReadonlyArray<NamedPostCompactionHook> {
|
|
918
|
+
if (input === undefined) {
|
|
919
|
+
return Object.freeze([
|
|
920
|
+
reanchorProjectRules(),
|
|
921
|
+
reanchorPersonaBlock(),
|
|
922
|
+
reanchorPinnedFacts({ pinnedFactIds: [] }),
|
|
923
|
+
]);
|
|
924
|
+
}
|
|
925
|
+
return Object.freeze(
|
|
926
|
+
input.map((hook, idx): NamedPostCompactionHook => {
|
|
927
|
+
if (typeof hook === 'function') {
|
|
928
|
+
const id = `customHook_${idx}`;
|
|
929
|
+
return {
|
|
930
|
+
id,
|
|
931
|
+
async resolveContent(_deps, ctx) {
|
|
932
|
+
// CE-6: the operator's hook sees the genuine compaction
|
|
933
|
+
// context. `ctx` is always supplied by compactNow; the
|
|
934
|
+
// throw guards a direct caller that forgot it rather than
|
|
935
|
+
// silently fabricating zeros (the old behaviour).
|
|
936
|
+
if (ctx === undefined) {
|
|
937
|
+
throw new TypeError(
|
|
938
|
+
'[graphorin/memory] post-compaction hooks require the compaction context - call through ContextEngine.compactNow(...).',
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const result = await hook(ctx);
|
|
942
|
+
return result;
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
return hook;
|
|
947
|
+
}),
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Augment a {@link MemoryMetadata} snapshot with the active locale +
|
|
953
|
+
* embedder tags so the rendered metadata block carries the
|
|
954
|
+
* additional context the model uses for reasoning. Pure: the
|
|
955
|
+
* original `meta` reference is not mutated.
|
|
956
|
+
*/
|
|
957
|
+
function enrichMetadataTags(
|
|
958
|
+
meta: import('@graphorin/core').MemoryMetadata,
|
|
959
|
+
localeId: string,
|
|
960
|
+
embedderId: string | null,
|
|
961
|
+
): import('@graphorin/core').MemoryMetadata {
|
|
962
|
+
const baseTags = meta.tags !== undefined ? [...meta.tags] : [];
|
|
963
|
+
if (!baseTags.some((tag) => tag.startsWith('locale:'))) {
|
|
964
|
+
baseTags.push(`locale:${localeId}`);
|
|
965
|
+
}
|
|
966
|
+
if (embedderId !== null && !baseTags.some((tag) => tag.startsWith('embedder:'))) {
|
|
967
|
+
baseTags.push(`embedder:${embedderId}`);
|
|
968
|
+
}
|
|
969
|
+
return { ...meta, tags: Object.freeze(baseTags) };
|
|
970
|
+
}
|