@graphorin/memory 0.6.1 → 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 +71 -0
- package/README.md +1 -1
- 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 +16 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- 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 +1 -1
- package/dist/package.js.map +1 -1
- 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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Priority-ordered token-budget allocator. The allocator runs over the six
|
|
3
|
+
* layers of the {@link ContextEngine}'s system prompt and trims the
|
|
4
|
+
* lowest-priority layers first when the combined cost exceeds the configured
|
|
5
|
+
* budget.
|
|
6
|
+
*
|
|
7
|
+
* This **truncation priority** is deliberately separate from the **emission
|
|
8
|
+
* order** (CE-9): the engine emits the surviving layers in a stable order
|
|
9
|
+
* (identity → rules → blocks → skills → [KV-cache breakpoint] → metadata →
|
|
10
|
+
* auto-recall) regardless of which were trimmed, so the cacheable prefix stays
|
|
11
|
+
* byte-stable across turns. This ladder governs only *what gets dropped first*.
|
|
12
|
+
*
|
|
13
|
+
* Priority (configurable), highest preserved → first truncated:
|
|
14
|
+
*
|
|
15
|
+
* 1. **identity** - Layer 1 (`graphorin_memory_base` + agent
|
|
16
|
+
* instructions). Always preserved; truncated last.
|
|
17
|
+
* 2. **memoryMetadata** - Layer 5. Small (~100-300 tokens).
|
|
18
|
+
* 3. **activeRules** - Layer 4 procedural rules.
|
|
19
|
+
* 4. **workingBlocks** - Layer 3.
|
|
20
|
+
* 5. **activeSkills** - Layer 4 skills metadata cards.
|
|
21
|
+
* 6. **autoRecall** - Layer 6. Lowest priority; truncated first.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ContextTokenCounter } from './token-counter.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Layer-id discriminator. Mirrors the documented Layer 1-6
|
|
30
|
+
* structure of the layered system prompt.
|
|
31
|
+
*
|
|
32
|
+
* @stable
|
|
33
|
+
*/
|
|
34
|
+
export type LayerId =
|
|
35
|
+
| 'identity'
|
|
36
|
+
| 'memoryMetadata'
|
|
37
|
+
| 'activeRules'
|
|
38
|
+
| 'workingBlocks'
|
|
39
|
+
| 'activeSkills'
|
|
40
|
+
| 'autoRecall';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default priority ladder. Higher priority means the layer is
|
|
44
|
+
* preserved longer when the budget is tight.
|
|
45
|
+
*
|
|
46
|
+
* @stable
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_LAYER_PRIORITY: ReadonlyArray<LayerId> = Object.freeze([
|
|
49
|
+
'identity',
|
|
50
|
+
'memoryMetadata',
|
|
51
|
+
'activeRules',
|
|
52
|
+
'workingBlocks',
|
|
53
|
+
'activeSkills',
|
|
54
|
+
'autoRecall',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Single layer candidate fed to {@link allocate}. The `text` field
|
|
59
|
+
* carries the rendered fragment; `cap` is the optional per-layer
|
|
60
|
+
* upper bound (in tokens); `priority` overrides the default ladder
|
|
61
|
+
* for advanced use cases.
|
|
62
|
+
*
|
|
63
|
+
* @stable
|
|
64
|
+
*/
|
|
65
|
+
export interface LayerCandidate {
|
|
66
|
+
readonly id: LayerId;
|
|
67
|
+
readonly text: string;
|
|
68
|
+
readonly cap?: number;
|
|
69
|
+
readonly priority?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Per-layer truncation mode used when the layer overflows its cap
|
|
74
|
+
* or the global budget. `truncate` is the default; `drop` removes
|
|
75
|
+
* the layer entirely when it would otherwise overflow.
|
|
76
|
+
*
|
|
77
|
+
* @stable
|
|
78
|
+
*/
|
|
79
|
+
export type OverflowMode = 'truncate' | 'drop';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Output of {@link allocate} - one entry per surviving layer in
|
|
83
|
+
* priority order, plus a `truncated` flag for observability.
|
|
84
|
+
*
|
|
85
|
+
* @stable
|
|
86
|
+
*/
|
|
87
|
+
export interface LayerAllocation {
|
|
88
|
+
readonly id: LayerId;
|
|
89
|
+
readonly text: string;
|
|
90
|
+
readonly tokens: number;
|
|
91
|
+
readonly truncated: boolean;
|
|
92
|
+
readonly droppedTokens: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Result of an allocator run.
|
|
97
|
+
*
|
|
98
|
+
* @stable
|
|
99
|
+
*/
|
|
100
|
+
export interface AllocationResult {
|
|
101
|
+
readonly layers: ReadonlyArray<LayerAllocation>;
|
|
102
|
+
readonly totalTokens: number;
|
|
103
|
+
readonly budgetTokens: number;
|
|
104
|
+
readonly overflowDropped: ReadonlyArray<LayerId>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Tags opened-but-not-closed in `text`, innermost-first. A heuristic
|
|
109
|
+
* scanner sized for the engine's own XML-ish layer markup
|
|
110
|
+
* (`<memory_blocks>`, `<memory_rules>`, ...) - self-closing tags are
|
|
111
|
+
* skipped and an unmatched closer is ignored.
|
|
112
|
+
*/
|
|
113
|
+
function unclosedTags(text: string): string[] {
|
|
114
|
+
const stack: string[] = [];
|
|
115
|
+
const re = /<(\/?)([A-Za-z_][\w.-]*)(?:\s[^<>]*)?(\/?)>/g;
|
|
116
|
+
let match = re.exec(text);
|
|
117
|
+
while (match !== null) {
|
|
118
|
+
const [, closing, name, selfClosing] = match;
|
|
119
|
+
if (selfClosing !== '/' && name !== undefined) {
|
|
120
|
+
if (closing === '/') {
|
|
121
|
+
const idx = stack.lastIndexOf(name);
|
|
122
|
+
if (idx !== -1) stack.splice(idx, 1);
|
|
123
|
+
} else {
|
|
124
|
+
stack.push(name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
match = re.exec(text);
|
|
128
|
+
}
|
|
129
|
+
return stack.reverse();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Snap a prefix cut to a structure-safe point (CE-16e): never end
|
|
134
|
+
* inside a partially-emitted tag (`...<memory_blo`).
|
|
135
|
+
*/
|
|
136
|
+
function snapCut(kept: string): string {
|
|
137
|
+
const lastOpen = kept.lastIndexOf('<');
|
|
138
|
+
return lastOpen > kept.lastIndexOf('>') ? kept.slice(0, lastOpen) : kept;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Truncate `text` to fit `maxTokens`, preserving the leading
|
|
143
|
+
* portion and replacing the trailing portion with the literal
|
|
144
|
+
* `[...truncated]` marker. The token estimate is computed via the
|
|
145
|
+
* supplied `counter`; truncation falls back to character-based
|
|
146
|
+
* trimming when the estimate is non-monotonic.
|
|
147
|
+
*
|
|
148
|
+
* Structure-aware (CE-16e): the cut never splits a tag, and block
|
|
149
|
+
* tags the cut leaves open are re-closed after the marker, so a
|
|
150
|
+
* capped layer of XML-ish markup (e.g. `<memory_blocks>`) stays
|
|
151
|
+
* well-formed in the assembled prompt. Plain strings are unaffected.
|
|
152
|
+
*
|
|
153
|
+
* @stable
|
|
154
|
+
*/
|
|
155
|
+
export async function truncateToTokens(
|
|
156
|
+
text: string,
|
|
157
|
+
maxTokens: number,
|
|
158
|
+
counter: ContextTokenCounter,
|
|
159
|
+
): Promise<{ readonly text: string; readonly tokens: number; readonly truncated: boolean }> {
|
|
160
|
+
if (maxTokens <= 0) return { text: '', tokens: 0, truncated: text.length > 0 };
|
|
161
|
+
const initial = await counter.countText(text);
|
|
162
|
+
if (initial <= maxTokens) return { text, tokens: initial, truncated: false };
|
|
163
|
+
const marker = '\n[...truncated]';
|
|
164
|
+
const markerTokens = await counter.countText(marker);
|
|
165
|
+
const targetTokens = Math.max(0, maxTokens - markerTokens);
|
|
166
|
+
if (targetTokens <= 0) return { text: marker, tokens: markerTokens, truncated: true };
|
|
167
|
+
// Approximate the cut point via the inverse of the heuristic
|
|
168
|
+
// chars-per-token ratio. Falls back to repeated halving when the
|
|
169
|
+
// ratio is non-monotonic for the supplied counter.
|
|
170
|
+
let lo = 0;
|
|
171
|
+
let hi = text.length;
|
|
172
|
+
let bestCut = 0;
|
|
173
|
+
while (lo <= hi) {
|
|
174
|
+
const mid = (lo + hi) >>> 1;
|
|
175
|
+
const slice = text.slice(0, mid);
|
|
176
|
+
const tokens = await counter.countText(slice);
|
|
177
|
+
if (tokens <= targetTokens) {
|
|
178
|
+
bestCut = mid;
|
|
179
|
+
lo = mid + 1;
|
|
180
|
+
} else {
|
|
181
|
+
hi = mid - 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let kept = snapCut(text.slice(0, bestCut));
|
|
185
|
+
let closers = unclosedTags(kept)
|
|
186
|
+
.map((t) => `</${t}>`)
|
|
187
|
+
.join('');
|
|
188
|
+
let truncated = `${kept}${marker}${closers.length > 0 ? `\n${closers}` : ''}`;
|
|
189
|
+
let tokens = await counter.countText(truncated);
|
|
190
|
+
// Re-closing may push past the cap - shave the kept body until the
|
|
191
|
+
// whole thing (body + marker + closers) fits. Terminates: `kept`
|
|
192
|
+
// shrinks by ≥1 char per pass, and the empty body fits by the
|
|
193
|
+
// `targetTokens > 0` guard above.
|
|
194
|
+
while (tokens > maxTokens && kept.length > 0) {
|
|
195
|
+
kept = snapCut(kept.slice(0, kept.length - Math.ceil(kept.length * 0.1)));
|
|
196
|
+
closers = unclosedTags(kept)
|
|
197
|
+
.map((t) => `</${t}>`)
|
|
198
|
+
.join('');
|
|
199
|
+
truncated = `${kept}${marker}${closers.length > 0 ? `\n${closers}` : ''}`;
|
|
200
|
+
tokens = await counter.countText(truncated);
|
|
201
|
+
}
|
|
202
|
+
return { text: truncated, tokens, truncated: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run the allocator. Layers are sorted by priority ascending (the
|
|
207
|
+
* first layer is the highest priority); when the running total
|
|
208
|
+
* exceeds the budget, lower-priority layers are first capped at
|
|
209
|
+
* their per-layer `cap` (when set) and finally truncated to
|
|
210
|
+
* whatever fits.
|
|
211
|
+
*
|
|
212
|
+
* @stable
|
|
213
|
+
*/
|
|
214
|
+
export async function allocate(
|
|
215
|
+
layers: ReadonlyArray<LayerCandidate>,
|
|
216
|
+
budgetTokens: number,
|
|
217
|
+
counter: ContextTokenCounter,
|
|
218
|
+
options: {
|
|
219
|
+
readonly priority?: ReadonlyArray<LayerId>;
|
|
220
|
+
readonly overflowMode?: OverflowMode;
|
|
221
|
+
} = {},
|
|
222
|
+
): Promise<AllocationResult> {
|
|
223
|
+
const priorityOrder = options.priority ?? DEFAULT_LAYER_PRIORITY;
|
|
224
|
+
const overflowMode = options.overflowMode ?? 'truncate';
|
|
225
|
+
const orderIndex = new Map<LayerId, number>();
|
|
226
|
+
priorityOrder.forEach((id, idx) => {
|
|
227
|
+
orderIndex.set(id, idx);
|
|
228
|
+
});
|
|
229
|
+
const sorted = [...layers].sort((a, b) => {
|
|
230
|
+
const aPriority = a.priority ?? orderIndex.get(a.id) ?? 999;
|
|
231
|
+
const bPriority = b.priority ?? orderIndex.get(b.id) ?? 999;
|
|
232
|
+
return aPriority - bPriority;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// First pass: enforce per-layer caps (regardless of total budget).
|
|
236
|
+
const capped: Array<{
|
|
237
|
+
readonly candidate: LayerCandidate;
|
|
238
|
+
text: string;
|
|
239
|
+
tokens: number;
|
|
240
|
+
truncated: boolean;
|
|
241
|
+
}> = [];
|
|
242
|
+
for (const candidate of sorted) {
|
|
243
|
+
if (candidate.text.length === 0) {
|
|
244
|
+
capped.push({ candidate, text: '', tokens: 0, truncated: false });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (candidate.cap !== undefined && candidate.cap >= 0) {
|
|
248
|
+
const trimmed = await truncateToTokens(candidate.text, candidate.cap, counter);
|
|
249
|
+
capped.push({
|
|
250
|
+
candidate,
|
|
251
|
+
text: trimmed.text,
|
|
252
|
+
tokens: trimmed.tokens,
|
|
253
|
+
truncated: trimmed.truncated,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
const tokens = await counter.countText(candidate.text);
|
|
257
|
+
capped.push({ candidate, text: candidate.text, tokens, truncated: false });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Second pass: enforce the global budget. Walk the layers in
|
|
262
|
+
// priority order, accumulating tokens; when the running total
|
|
263
|
+
// exceeds the budget, the rest are dropped or truncated to fit.
|
|
264
|
+
const budget = Math.max(0, budgetTokens);
|
|
265
|
+
const layersOut: LayerAllocation[] = [];
|
|
266
|
+
const overflowDropped: LayerId[] = [];
|
|
267
|
+
let runningTotal = 0;
|
|
268
|
+
for (const entry of capped) {
|
|
269
|
+
if (entry.text.length === 0) {
|
|
270
|
+
layersOut.push({
|
|
271
|
+
id: entry.candidate.id,
|
|
272
|
+
text: '',
|
|
273
|
+
tokens: 0,
|
|
274
|
+
truncated: false,
|
|
275
|
+
droppedTokens: 0,
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (runningTotal + entry.tokens <= budget) {
|
|
280
|
+
runningTotal += entry.tokens;
|
|
281
|
+
layersOut.push({
|
|
282
|
+
id: entry.candidate.id,
|
|
283
|
+
text: entry.text,
|
|
284
|
+
tokens: entry.tokens,
|
|
285
|
+
truncated: entry.truncated,
|
|
286
|
+
droppedTokens: 0,
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const remaining = Math.max(0, budget - runningTotal);
|
|
291
|
+
if (overflowMode === 'drop' || remaining === 0) {
|
|
292
|
+
overflowDropped.push(entry.candidate.id);
|
|
293
|
+
layersOut.push({
|
|
294
|
+
id: entry.candidate.id,
|
|
295
|
+
text: '',
|
|
296
|
+
tokens: 0,
|
|
297
|
+
truncated: false,
|
|
298
|
+
droppedTokens: entry.tokens,
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const trimmed = await truncateToTokens(entry.text, remaining, counter);
|
|
303
|
+
runningTotal += trimmed.tokens;
|
|
304
|
+
layersOut.push({
|
|
305
|
+
id: entry.candidate.id,
|
|
306
|
+
text: trimmed.text,
|
|
307
|
+
tokens: trimmed.tokens,
|
|
308
|
+
truncated: true,
|
|
309
|
+
droppedTokens: entry.tokens - trimmed.tokens,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
layers: Object.freeze(layersOut),
|
|
315
|
+
totalTokens: runningTotal,
|
|
316
|
+
budgetTokens: budget,
|
|
317
|
+
overflowDropped: Object.freeze(overflowDropped),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-counting helpers used by the {@link ContextEngine}. The
|
|
3
|
+
* production path accepts a {@link TokenCounter} from
|
|
4
|
+
* `@graphorin/core` (the default `JsTiktokenCounter` lives in
|
|
5
|
+
* `@graphorin/provider`); the fallback path is a deterministic
|
|
6
|
+
* heuristic that keeps unit tests dependency-free.
|
|
7
|
+
*
|
|
8
|
+
* The fallback counts ~1 token per 4 characters - close enough for
|
|
9
|
+
* budgeting decisions, intentionally less precise than a real BPE
|
|
10
|
+
* tokenizer. Production deployments wire a real
|
|
11
|
+
* {@link TokenCounter} through `createMemory({ contextEngine: {
|
|
12
|
+
* tokenCounter: ... } })`.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Message, TokenCounter } from '@graphorin/core';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pluggable text-token counter used inside the ContextEngine. The
|
|
21
|
+
* surface is narrower than {@link TokenCounter} because the engine
|
|
22
|
+
* never assembles a message list during the budget-allocation
|
|
23
|
+
* phase - it operates on rendered text fragments.
|
|
24
|
+
*
|
|
25
|
+
* @stable
|
|
26
|
+
*/
|
|
27
|
+
export interface ContextTokenCounter {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
countText(text: string): Promise<number>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Script-aware heuristic (CE-13). The flat chars/4 rule undercounts
|
|
34
|
+
* non-Latin scripts ~4x - real tokenizers emit roughly one token per
|
|
35
|
+
* CJK character (and per kana / hangul syllable), so a 200k-window
|
|
36
|
+
* threshold expressed in "heuristic tokens" let CJK conversations blow
|
|
37
|
+
* past the provider limit before compaction ever fired. Characters
|
|
38
|
+
* outside the Latin-ish range count at one token each; Latin text keeps
|
|
39
|
+
* the classic chars/4. Still a heuristic - wire a real `TokenCounter`
|
|
40
|
+
* (e.g. the provider package's JsTiktokenCounter) for production
|
|
41
|
+
* accuracy.
|
|
42
|
+
*/
|
|
43
|
+
const DENSE_SCRIPT_RE =
|
|
44
|
+
// CJK unified + extensions, kana, hangul, CJK punctuation, full-width forms.
|
|
45
|
+
/[ᄀ-ᇿ⺀-〿-ヿ-ㇰ-䶿一-鿿ꥠ-가-豈--]/g;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Built-in heuristic counter - chars/4 for Latin-ish text plus one
|
|
49
|
+
* token per dense-script (CJK/kana/hangul) character (CE-13). Stable
|
|
50
|
+
* default when the operator does not pass a real {@link TokenCounter}.
|
|
51
|
+
*
|
|
52
|
+
* @stable
|
|
53
|
+
*/
|
|
54
|
+
export const HEURISTIC_TOKEN_COUNTER: ContextTokenCounter = Object.freeze({
|
|
55
|
+
id: 'graphorin/heuristic@chars-per-4+dense-script',
|
|
56
|
+
async countText(text: string): Promise<number> {
|
|
57
|
+
if (typeof text !== 'string' || text.length === 0) return 0;
|
|
58
|
+
const dense = text.match(DENSE_SCRIPT_RE)?.length ?? 0;
|
|
59
|
+
const latin = text.length - dense;
|
|
60
|
+
return Math.ceil(latin / 4) + dense;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Wrap a real {@link TokenCounter} into the narrower
|
|
66
|
+
* {@link ContextTokenCounter} surface - PRESERVING the native
|
|
67
|
+
* message-level `count(messages)` (context-engine-03). The adapter
|
|
68
|
+
* used to keep only `countText`, which forced
|
|
69
|
+
* {@link countMessageTokens} onto the per-message render path for
|
|
70
|
+
* every real counter; combined with `renderMessageText` ignoring
|
|
71
|
+
* tool calls, tool-call arguments contributed zero to every trigger /
|
|
72
|
+
* before / after count while the provider billed for them.
|
|
73
|
+
*
|
|
74
|
+
* @stable
|
|
75
|
+
*/
|
|
76
|
+
export function adaptTokenCounter(
|
|
77
|
+
counter: TokenCounter,
|
|
78
|
+
): ContextTokenCounter & Pick<TokenCounter, 'count'> {
|
|
79
|
+
return Object.freeze({
|
|
80
|
+
id: counter.id,
|
|
81
|
+
count: (messages: ReadonlyArray<Message>): Promise<number> => counter.count(messages),
|
|
82
|
+
async countText(text: string): Promise<number> {
|
|
83
|
+
if (typeof text !== 'string' || text.length === 0) return 0;
|
|
84
|
+
return counter.countText(text);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Count tokens across a message list using a {@link TokenCounter}
|
|
91
|
+
* (or the heuristic fallback). Used by the trigger-evaluation hot
|
|
92
|
+
* path of the auto-compaction subsystem (RB-46) at the top of every
|
|
93
|
+
* step.
|
|
94
|
+
*
|
|
95
|
+
* @stable
|
|
96
|
+
*/
|
|
97
|
+
export async function countMessageTokens(
|
|
98
|
+
messages: ReadonlyArray<Message>,
|
|
99
|
+
counter: TokenCounter | ContextTokenCounter,
|
|
100
|
+
): Promise<number> {
|
|
101
|
+
if ('count' in counter && typeof (counter as TokenCounter).count === 'function') {
|
|
102
|
+
return (counter as TokenCounter).count(messages);
|
|
103
|
+
}
|
|
104
|
+
let total = 0;
|
|
105
|
+
for (const message of messages) {
|
|
106
|
+
total += await counter.countText(renderMessageText(message));
|
|
107
|
+
}
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Render a `Message` into a single textual approximation suitable
|
|
113
|
+
* for token counting. Multimodal parts other than `'text'` /
|
|
114
|
+
* `'reasoning'` contribute a constant approximation so the counter
|
|
115
|
+
* does not silently under-count. Assistant tool calls render their
|
|
116
|
+
* name + serialized args (context-engine-03) - file writes and
|
|
117
|
+
* `code_execute` scripts are frequently the dominant tokens of an
|
|
118
|
+
* agentic step, and the provider serializes + counts them, so the
|
|
119
|
+
* engine's arithmetic must too (mirrors the provider-side
|
|
120
|
+
* `serialiseMessageForCount`).
|
|
121
|
+
*
|
|
122
|
+
* @stable
|
|
123
|
+
*/
|
|
124
|
+
export function renderMessageText(message: Message): string {
|
|
125
|
+
if (message.role === 'system') return message.content;
|
|
126
|
+
const content = message.content;
|
|
127
|
+
let out = '';
|
|
128
|
+
if (typeof content === 'string') {
|
|
129
|
+
out = content;
|
|
130
|
+
} else {
|
|
131
|
+
for (const part of content) {
|
|
132
|
+
if (part.type === 'text' || part.type === 'reasoning') {
|
|
133
|
+
out += `${part.text}\n`;
|
|
134
|
+
} else {
|
|
135
|
+
// Approximate non-text parts (image / audio / file) as a small
|
|
136
|
+
// fixed cost so the counter does not silently under-count.
|
|
137
|
+
out += '[non-text-part]\n';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (message.role === 'assistant' && message.toolCalls !== undefined) {
|
|
142
|
+
for (const call of message.toolCalls) {
|
|
143
|
+
const args = typeof call.args === 'string' ? call.args : safeStringifyArgs(call.args);
|
|
144
|
+
out += `\n[tool-call:${call.toolName}] ${args}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** JSON-stringify tool args without ever throwing (circular → constant). */
|
|
151
|
+
function safeStringifyArgs(args: unknown): string {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.stringify(args) ?? '';
|
|
154
|
+
} catch {
|
|
155
|
+
return '[unserializable-args]';
|
|
156
|
+
}
|
|
157
|
+
}
|