@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.
Files changed (227) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +24 -7
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +17 -15
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -7
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +6 -0
  89. package/dist/package.js.map +1 -0
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Cooperation contract between the {@link ContextEngine} and the
3
+ * outbound prompt-redaction middleware (D3, see ADR-045 / DEC-157)
4
+ * + the inbound tool-result sanitization middleware (D4, see
5
+ * RB-43 / DEC-159).
6
+ *
7
+ * Every {@link MessageContent} part assembled by the ContextEngine
8
+ * carries two **independent** annotations:
9
+ *
10
+ * - `graphorin.content.origin` - where the content came from. Used
11
+ * by D3 (`withRedaction`) under `scanScope: 'untrusted'` to
12
+ * decide whether to re-scan a part that already passed the D2
13
+ * sensitivity-tier filter.
14
+ * - `graphorin.content.inbound.trust` - what trust class the source
15
+ * carries. Used by D4 (`withInboundSanitization`) and by Phase 12
16
+ * (agent runtime) to gate the per-step preamble injection.
17
+ *
18
+ * Both annotations live as **span attributes only** (observability)
19
+ * - they are never serialized to the wire payload. The wire-stable
20
+ * `ProviderRequest` shape is unchanged by Phase 10d.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+
25
+ /**
26
+ * Span-attribute key for the origin axis.
27
+ *
28
+ * @stable
29
+ */
30
+ export const CONTENT_ORIGIN_ATTR = 'graphorin.content.origin' as const;
31
+
32
+ /**
33
+ * Span-attribute key for the inbound-trust axis.
34
+ *
35
+ * @stable
36
+ */
37
+ export const INBOUND_TRUST_ATTR = 'graphorin.content.inbound.trust' as const;
38
+
39
+ /**
40
+ * Origin discriminator for an assembled message-content part. The
41
+ * non-ContextEngine origins (`'user:input'`, `'tool:result'`,
42
+ * `'mcp:response'`, `'tool-call:args'`) are tagged by Phase 12
43
+ * (agent runtime) when the corresponding payload enters
44
+ * `session_messages`; the ContextEngine then propagates the tag
45
+ * through the assembled message list.
46
+ *
47
+ * @stable
48
+ */
49
+ export type ContentOrigin =
50
+ | 'memory:tier-filtered'
51
+ | 'system:framework'
52
+ | 'agent:instructions'
53
+ | 'skill:content'
54
+ | 'user:input'
55
+ | 'tool:result'
56
+ | 'mcp:response'
57
+ | 'tool-call:args';
58
+
59
+ /**
60
+ * Trust-class discriminator for an assembled message-content part.
61
+ * Sibling axis to {@link ContentOrigin}; the two are independent.
62
+ *
63
+ * - `'trusted'` - built-in framework tools + trusted-skill-bundled
64
+ * tools; D4 preamble does NOT fire on steps containing only
65
+ * these parts.
66
+ * - `'user-defined'` - tools registered via `tool({...})` from user
67
+ * application code; D4 preamble fires.
68
+ * - `'untrusted-skill'` - tools bundled by an untrusted skill; D4
69
+ * preamble fires; default policy is strip-and-wrap.
70
+ * - `'mcp'` - every `Tool` produced by `MCPClient.toTools(...)`; D4
71
+ * preamble fires; default policy is strip-and-wrap.
72
+ * - `'web-search'` - built-in `web_search` adapter; D4 preamble
73
+ * fires; default policy is strip-and-wrap.
74
+ * - `'n/a'` - non-tool-result parts for which the inbound-trust
75
+ * axis is meaningless (`'user:input'`, `'memory:tier-filtered'`,
76
+ * `'system:framework'`, `'agent:instructions'`,
77
+ * `'tool-call:args'`).
78
+ *
79
+ * @stable
80
+ */
81
+ export type InboundTrust =
82
+ | 'trusted'
83
+ | 'user-defined'
84
+ | 'untrusted-skill'
85
+ | 'mcp'
86
+ | 'web-search'
87
+ | 'n/a';
88
+
89
+ /**
90
+ * Origins for which the inbound-trust axis is meaningless and is
91
+ * always set to `'n/a'`. Surfaced as a frozen constant so consumers
92
+ * can introspect the contract without re-implementing the rule.
93
+ *
94
+ * @stable
95
+ */
96
+ export const NON_INBOUND_ORIGINS: ReadonlySet<ContentOrigin> = Object.freeze(
97
+ new Set<ContentOrigin>([
98
+ 'memory:tier-filtered',
99
+ 'system:framework',
100
+ 'agent:instructions',
101
+ 'user:input',
102
+ 'tool-call:args',
103
+ ]),
104
+ );
105
+
106
+ /**
107
+ * Single typed annotation for an assembled message-content part.
108
+ *
109
+ * @stable
110
+ */
111
+ export interface ContentAnnotation {
112
+ readonly origin: ContentOrigin;
113
+ readonly inboundTrust: InboundTrust;
114
+ }
115
+
116
+ /**
117
+ * Build an annotation, enforcing the rule that
118
+ * non-tool-result origins always carry `inboundTrust: 'n/a'`. A
119
+ * caller that requests a non-`'n/a'` value for a non-inbound origin
120
+ * is silently corrected (defense-in-depth: the rule is enforced
121
+ * here so callers cannot accidentally violate it).
122
+ *
123
+ * @stable
124
+ */
125
+ export function annotate(origin: ContentOrigin, inboundTrust: InboundTrust): ContentAnnotation {
126
+ if (NON_INBOUND_ORIGINS.has(origin)) {
127
+ return Object.freeze({ origin, inboundTrust: 'n/a' });
128
+ }
129
+ return Object.freeze({ origin, inboundTrust });
130
+ }
131
+
132
+ /**
133
+ * Convert a {@link ContentAnnotation} to a span-attributes record
134
+ * suitable for `AISpan.setAttributes(...)`.
135
+ *
136
+ * @stable
137
+ */
138
+ export function toSpanAttributes(annotation: ContentAnnotation): Readonly<Record<string, string>> {
139
+ return Object.freeze({
140
+ [CONTENT_ORIGIN_ATTR]: annotation.origin,
141
+ [INBOUND_TRUST_ATTR]: annotation.inboundTrust,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Decide whether the per-step inbound-sanitization preamble (D4)
147
+ * should fire for an assembled message list. The preamble fires
148
+ * iff at least one part carries an inbound-trust value other than
149
+ * `'trusted'` and `'n/a'`. Trusted-only steps skip the preamble for
150
+ * cache-friendliness; preamble is emitted exactly once per step
151
+ * regardless of how many untrusted parts the step carries.
152
+ *
153
+ * @stable
154
+ */
155
+ export function shouldFireInboundPreamble(annotations: ReadonlyArray<ContentAnnotation>): boolean {
156
+ for (const annotation of annotations) {
157
+ const trust = annotation.inboundTrust;
158
+ if (trust !== 'trusted' && trust !== 'n/a') return true;
159
+ }
160
+ return false;
161
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Auto-recall heuristic - opt-in regex-based pre-call that injects
3
+ * the top-K facts into Layer 6 when the last user message matches one
4
+ * of the configured trigger phrases. (Episode auto-recall is not
5
+ * implemented; episodes are reachable via the `recall_episodes` tool.)
6
+ *
7
+ * The framework ships English defaults; per-locale trigger
8
+ * phrases extend the default via {@link defineContextLocalePack}.
9
+ * Application code can also register a fully-custom strategy via
10
+ * {@link defineAutoRecallStrategy}.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import type { ContextLocalePack } from './locale-packs/index.js';
16
+
17
+ /**
18
+ * Per-call scope passed to a custom strategy.
19
+ *
20
+ * @stable
21
+ */
22
+ export interface AutoRecallStrategyContext {
23
+ readonly locale: string;
24
+ readonly lastUserMessage: string;
25
+ }
26
+
27
+ /**
28
+ * Outcome of a strategy invocation.
29
+ *
30
+ * @stable
31
+ */
32
+ export interface AutoRecallTriggerResult {
33
+ readonly factsTriggered: boolean;
34
+ /** Optional reason surfaced in spans / metadata. */
35
+ readonly reason?: string;
36
+ }
37
+
38
+ /**
39
+ * Pluggable strategy signature.
40
+ *
41
+ * @stable
42
+ */
43
+ export type AutoRecallStrategy = (ctx: AutoRecallStrategyContext) => AutoRecallTriggerResult;
44
+
45
+ /**
46
+ * Build a locale-driven heuristic strategy. The default for every
47
+ * configured locale.
48
+ *
49
+ * @stable
50
+ */
51
+ export function defaultLocaleHeuristicStrategy(pack: ContextLocalePack): AutoRecallStrategy {
52
+ const factTriggers = pack.autoRecallTriggers.factTriggers;
53
+ return (ctx) => {
54
+ const text = ctx.lastUserMessage ?? '';
55
+ if (text.length === 0) {
56
+ return { factsTriggered: false };
57
+ }
58
+ const factsTriggered = factTriggers.some((re) => re.test(text));
59
+ if (factsTriggered) {
60
+ return { factsTriggered, reason: 'locale-heuristic-trigger-matched' };
61
+ }
62
+ return { factsTriggered: false };
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Builder for application-supplied custom strategies. Accepts a
68
+ * raw function and returns a tagged version so the engine can
69
+ * surface the strategy name on spans.
70
+ *
71
+ * @stable
72
+ */
73
+ export function defineAutoRecallStrategy(opts: {
74
+ readonly id: string;
75
+ readonly evaluate: AutoRecallStrategy;
76
+ }): AutoRecallStrategy & { readonly id: string } {
77
+ if (typeof opts.id !== 'string' || opts.id.length === 0) {
78
+ throw new TypeError(
79
+ '[graphorin/memory] defineAutoRecallStrategy: `id` must be a non-empty string.',
80
+ );
81
+ }
82
+ if (typeof opts.evaluate !== 'function') {
83
+ throw new TypeError(
84
+ '[graphorin/memory] defineAutoRecallStrategy: `evaluate` must be a function.',
85
+ );
86
+ }
87
+ const wrapped = ((ctx: AutoRecallStrategyContext) => opts.evaluate(ctx)) as AutoRecallStrategy & {
88
+ readonly id: string;
89
+ };
90
+ Object.defineProperty(wrapped, 'id', { value: opts.id, enumerable: true });
91
+ return wrapped;
92
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * SOTA-1: zero-LLM tool-result clearing - the cheapest pre-compaction tier.
3
+ *
4
+ * Mirrors Anthropic context editing (`clear_tool_uses_20250919`): when the
5
+ * buffer crosses the compaction threshold, replace the oldest tool results with
6
+ * a compact placeholder BEFORE paying for a summarizer LLM call. The most recent
7
+ * `keepToolUses` results stay verbatim, `excludeTools` are never touched, and
8
+ * clearing is skipped entirely when it would reclaim fewer than `clearAtLeast`
9
+ * tokens (not worth the context churn). Pure + deterministic - no provider call.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import type { Message, ToolMessage } from '@graphorin/core';
15
+ import {
16
+ type ContextTokenCounter,
17
+ countMessageTokens,
18
+ renderMessageText,
19
+ } from '../token-counter.js';
20
+
21
+ /** Placeholder prefix on a cleared tool result - makes clearing idempotent. */
22
+ export const CLEARED_TOOL_RESULT_MARKER = '[cleared tool result';
23
+
24
+ /** Default count of most-recent tool results kept verbatim. */
25
+ export const DEFAULT_KEEP_TOOL_USES = 3;
26
+
27
+ /** Knobs for {@link clearOldToolResults} (mirrors the strategy variant). */
28
+ export interface ClearToolResultsOptions {
29
+ /** Most-recent tool results kept verbatim (default `DEFAULT_KEEP_TOOL_USES`). */
30
+ readonly keepToolUses?: number;
31
+ /** Only clear if at least this many tokens are reclaimable; else leave untouched (default 0). */
32
+ readonly clearAtLeast?: number;
33
+ /** Tool names whose results are never cleared. */
34
+ readonly excludeTools?: ReadonlyArray<string>;
35
+ /** Placeholder builder; defaults to a one-line `[cleared tool result …]` marker. */
36
+ readonly placeholder?: (info: {
37
+ readonly toolCallId: string;
38
+ readonly toolName?: string;
39
+ readonly clearedTokens: number;
40
+ }) => string;
41
+ /**
42
+ * A6 / SOTA-2 - recoverable clearing. When provided, the original tool-result
43
+ * text of each cleared message is handed to this callback (wire it to a spill
44
+ * store / the `read_result` handle registry) and the placeholder references the
45
+ * returned handle id + preview, so the model can re-fetch the full result via
46
+ * `read_result` rather than losing it. Invoked only for clears that actually
47
+ * commit (after the `clearAtLeast` floor), so a rejected clearing never spills.
48
+ * Omitted ⇒ the bare `placeholder` (irrecoverable, byte-identical default).
49
+ */
50
+ readonly externalize?: (
51
+ content: string,
52
+ info: {
53
+ readonly toolCallId: string;
54
+ readonly toolName?: string;
55
+ readonly clearedTokens: number;
56
+ },
57
+ ) => Promise<{ readonly handleId: string; readonly preview?: string }>;
58
+ /**
59
+ * C4 (context-engine-11): the tool the externalized-handle placeholder
60
+ * advertises. The memory package cannot know whether the agent
61
+ * registered `read_result` (that depends on spill wiring), so callers
62
+ * whose runtime does NOT expose it pass `null` and the placeholder
63
+ * degrades to a tool-neutral phrasing instead of promising a tool the
64
+ * model cannot call. Default `'read_result'` (the agent built-in).
65
+ */
66
+ readonly readResultToolName?: string | null;
67
+ /**
68
+ * C4 (clear_tool_uses_20250919 parity): additionally blank the PAIRED
69
+ * assistant message's tool-call arguments for every cleared result,
70
+ * reclaiming the input side of verbose calls too. Default `false`.
71
+ */
72
+ readonly clearToolInputs?: boolean;
73
+ }
74
+
75
+ /** Result of a clearing pass. `clearedIndices` empty ⇒ nothing changed. */
76
+ export interface ClearToolResultsOutcome {
77
+ readonly messages: ReadonlyArray<Message>;
78
+ readonly clearedIndices: ReadonlyArray<number>;
79
+ readonly reclaimedTokens: number;
80
+ }
81
+
82
+ function defaultPlaceholder(info: { toolName?: string; clearedTokens: number }): string {
83
+ return `${CLEARED_TOOL_RESULT_MARKER} · ${info.toolName ?? 'tool'} · ${info.clearedTokens} tokens elided · re-run the tool if needed]`;
84
+ }
85
+
86
+ /** A6: a placeholder that points at the externalized handle (recoverable via read_result). */
87
+ function handlePlaceholder(info: {
88
+ toolName?: string;
89
+ clearedTokens: number;
90
+ handleId: string;
91
+ preview?: string;
92
+ readToolName?: string | null;
93
+ }): string {
94
+ const preview = info.preview !== undefined && info.preview.length > 0 ? ` · ${info.preview}` : '';
95
+ // C4 (context-engine-11): only advertise a retrieval tool the caller
96
+ // vouches exists; `null` degrades to a tool-neutral phrasing.
97
+ const retrieval =
98
+ info.readToolName === null
99
+ ? `full result externalized to handle: ${info.handleId}`
100
+ : `full result via ${info.readToolName ?? 'read_result'} handle: ${info.handleId}`;
101
+ return `${CLEARED_TOOL_RESULT_MARKER} · ${info.toolName ?? 'tool'} · ${info.clearedTokens} tokens · ${retrieval}${preview}]`;
102
+ }
103
+
104
+ function isAlreadyCleared(content: Message['content']): boolean {
105
+ return typeof content === 'string' && content.startsWith(CLEARED_TOOL_RESULT_MARKER);
106
+ }
107
+
108
+ /**
109
+ * Replace the oldest clearable tool results with placeholders. Returns the new
110
+ * buffer (same length - content is replaced in place, never removed) plus the
111
+ * cleared indices and reclaimed token count. Idempotent: already-cleared
112
+ * placeholders are skipped on a second pass.
113
+ */
114
+ export async function clearOldToolResults(
115
+ messages: ReadonlyArray<Message>,
116
+ options: ClearToolResultsOptions,
117
+ counter: ContextTokenCounter,
118
+ ): Promise<ClearToolResultsOutcome> {
119
+ const keep = options.keepToolUses ?? DEFAULT_KEEP_TOOL_USES;
120
+ const exclude = new Set(options.excludeTools ?? []);
121
+ const placeholder = options.placeholder ?? defaultPlaceholder;
122
+
123
+ // toolCallId → toolName from every assistant tool-call in the buffer.
124
+ const toolNameById = new Map<string, string>();
125
+ for (const m of messages) {
126
+ if (m.role === 'assistant' && m.toolCalls) {
127
+ for (const tc of m.toolCalls) toolNameById.set(tc.toolCallId, tc.toolName);
128
+ }
129
+ }
130
+
131
+ // Indices of all not-yet-cleared tool-result messages, oldest first.
132
+ const toolIdx: number[] = [];
133
+ for (let i = 0; i < messages.length; i++) {
134
+ const m = messages[i];
135
+ if (m?.role === 'tool' && !isAlreadyCleared(m.content)) toolIdx.push(i);
136
+ }
137
+
138
+ // Keep the most-recent `keep`; older ones are candidates unless excluded.
139
+ const keptCutoff = toolIdx.length - Math.max(0, keep);
140
+ const replacements = new Map<number, ToolMessage>();
141
+ const clearedTokensByIdx = new Map<number, number>();
142
+ let reclaimable = 0;
143
+ for (let j = 0; j < toolIdx.length && j < keptCutoff; j++) {
144
+ const idx = toolIdx[j];
145
+ if (idx === undefined) continue;
146
+ const tm = messages[idx] as ToolMessage;
147
+ const name = toolNameById.get(tm.toolCallId);
148
+ if (name !== undefined && exclude.has(name)) continue;
149
+ const originalTokens = await countMessageTokens([tm], counter);
150
+ const replaced: ToolMessage = {
151
+ role: 'tool',
152
+ toolCallId: tm.toolCallId,
153
+ content: placeholder({
154
+ toolCallId: tm.toolCallId,
155
+ ...(name !== undefined ? { toolName: name } : {}),
156
+ clearedTokens: originalTokens,
157
+ }),
158
+ };
159
+ const placeholderTokens = await countMessageTokens([replaced], counter);
160
+ reclaimable += Math.max(0, originalTokens - placeholderTokens);
161
+ replacements.set(idx, replaced);
162
+ clearedTokensByIdx.set(idx, originalTokens);
163
+ }
164
+
165
+ const clearAtLeast = options.clearAtLeast ?? 0;
166
+ if (replacements.size === 0 || reclaimable < clearAtLeast) {
167
+ return { messages, clearedIndices: [], reclaimedTokens: 0 };
168
+ }
169
+
170
+ // A6: only now that the clearing commits, externalize each cleared result to a
171
+ // recoverable handle and rewrite its placeholder to reference it. Skipped
172
+ // clears (above) never reach here, so the spill never fires for nothing.
173
+ const externalize = options.externalize;
174
+ if (externalize !== undefined) {
175
+ for (const idx of replacements.keys()) {
176
+ const tm = messages[idx] as ToolMessage;
177
+ const name = toolNameById.get(tm.toolCallId);
178
+ const clearedTokens = clearedTokensByIdx.get(idx) ?? 0;
179
+ const content = typeof tm.content === 'string' ? tm.content : renderMessageText(tm);
180
+ const handle = await externalize(content, {
181
+ toolCallId: tm.toolCallId,
182
+ ...(name !== undefined ? { toolName: name } : {}),
183
+ clearedTokens,
184
+ });
185
+ replacements.set(idx, {
186
+ role: 'tool',
187
+ toolCallId: tm.toolCallId,
188
+ content: handlePlaceholder({
189
+ ...(name !== undefined ? { toolName: name } : {}),
190
+ clearedTokens,
191
+ handleId: handle.handleId,
192
+ ...(handle.preview !== undefined ? { preview: handle.preview } : {}),
193
+ ...(options.readResultToolName !== undefined
194
+ ? { readToolName: options.readResultToolName }
195
+ : {}),
196
+ }),
197
+ });
198
+ }
199
+ }
200
+
201
+ let out: Message[] = messages.map((m, i) => replacements.get(i) ?? m);
202
+
203
+ // C4 (clear_tool_uses parity): blank the paired assistant tool-call
204
+ // ARGUMENTS for every cleared result. The call's name and id survive
205
+ // (the transcript stays well-formed); only the argument payload is
206
+ // replaced with a marker object.
207
+ if (options.clearToolInputs === true && replacements.size > 0) {
208
+ const clearedCallIds = new Set<string>();
209
+ for (const idx of replacements.keys()) {
210
+ const tm = messages[idx] as ToolMessage;
211
+ clearedCallIds.add(tm.toolCallId);
212
+ }
213
+ out = out.map((m) => {
214
+ if (m.role !== 'assistant' || m.toolCalls === undefined) return m;
215
+ if (!m.toolCalls.some((tc) => clearedCallIds.has(tc.toolCallId))) return m;
216
+ return {
217
+ ...m,
218
+ toolCalls: m.toolCalls.map((tc) =>
219
+ clearedCallIds.has(tc.toolCallId)
220
+ ? { ...tc, args: { cleared: '[tool input elided by context clearing]' } }
221
+ : tc,
222
+ ),
223
+ };
224
+ });
225
+ }
226
+
227
+ return {
228
+ messages: out,
229
+ clearedIndices: [...replacements.keys()],
230
+ reclaimedTokens: reclaimable,
231
+ };
232
+ }