@graphorin/memory 0.5.0 → 0.6.1

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 (236) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/README.md +54 -37
  3. package/dist/conflict/index.d.ts +3 -1
  4. package/dist/conflict/index.js +1 -1
  5. package/dist/conflict/locale-packs/en.d.ts +13 -0
  6. package/dist/conflict/locale-packs/en.d.ts.map +1 -0
  7. package/dist/conflict/locale-packs/en.js +1 -1
  8. package/dist/conflict/locale-packs/en.js.map +1 -1
  9. package/dist/conflict/pipeline.d.ts +2 -2
  10. package/dist/conflict/pipeline.js +3 -3
  11. package/dist/conflict/pipeline.js.map +1 -1
  12. package/dist/conflict/stages/helpers.js +1 -1
  13. package/dist/conflict/stages/helpers.js.map +1 -1
  14. package/dist/conflict/stages/stage1-exact-dedup.js.map +1 -1
  15. package/dist/conflict/stages/stage2-embedding-three-zone.js.map +1 -1
  16. package/dist/conflict/stages/stage3-heuristic-regex.js +1 -1
  17. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  18. package/dist/conflict/stages/stage4-subject-predicate.js.map +1 -1
  19. package/dist/conflict/stages/stage5-defer-to-deep.js.map +1 -1
  20. package/dist/conflict/types.d.ts +7 -7
  21. package/dist/conflict/types.js +1 -1
  22. package/dist/conflict/types.js.map +1 -1
  23. package/dist/consolidator/budget.js +16 -3
  24. package/dist/consolidator/budget.js.map +1 -1
  25. package/dist/consolidator/decay.d.ts +13 -2
  26. package/dist/consolidator/decay.js +17 -5
  27. package/dist/consolidator/decay.js.map +1 -1
  28. package/dist/consolidator/dlq.js +1 -1
  29. package/dist/consolidator/dlq.js.map +1 -1
  30. package/dist/consolidator/errors.js +2 -2
  31. package/dist/consolidator/errors.js.map +1 -1
  32. package/dist/consolidator/index.js +8 -6
  33. package/dist/consolidator/index.js.map +1 -1
  34. package/dist/consolidator/lock.js +1 -1
  35. package/dist/consolidator/lock.js.map +1 -1
  36. package/dist/consolidator/noise-filter.js.map +1 -1
  37. package/dist/consolidator/phases/deep.js +6 -2
  38. package/dist/consolidator/phases/deep.js.map +1 -1
  39. package/dist/consolidator/phases/induce.d.ts +10 -10
  40. package/dist/consolidator/phases/induce.js +6 -6
  41. package/dist/consolidator/phases/induce.js.map +1 -1
  42. package/dist/consolidator/phases/learned-context.js +129 -0
  43. package/dist/consolidator/phases/learned-context.js.map +1 -0
  44. package/dist/consolidator/phases/light.js +2 -1
  45. package/dist/consolidator/phases/light.js.map +1 -1
  46. package/dist/consolidator/phases/reflect.js +5 -4
  47. package/dist/consolidator/phases/reflect.js.map +1 -1
  48. package/dist/consolidator/phases/standard.js +37 -8
  49. package/dist/consolidator/phases/standard.js.map +1 -1
  50. package/dist/consolidator/reconcile.js +1 -1
  51. package/dist/consolidator/reconcile.js.map +1 -1
  52. package/dist/consolidator/runtime.d.ts +4 -4
  53. package/dist/consolidator/runtime.d.ts.map +1 -1
  54. package/dist/consolidator/runtime.js +73 -16
  55. package/dist/consolidator/runtime.js.map +1 -1
  56. package/dist/consolidator/scheduler.d.ts +5 -5
  57. package/dist/consolidator/scheduler.js +1 -1
  58. package/dist/consolidator/scheduler.js.map +1 -1
  59. package/dist/consolidator/triggers.js +7 -7
  60. package/dist/consolidator/triggers.js.map +1 -1
  61. package/dist/consolidator/types.d.ts +68 -20
  62. package/dist/consolidator/types.d.ts.map +1 -1
  63. package/dist/consolidator/types.js +16 -6
  64. package/dist/consolidator/types.js.map +1 -1
  65. package/dist/context-engine/annotations.d.ts +9 -9
  66. package/dist/context-engine/annotations.js +3 -3
  67. package/dist/context-engine/annotations.js.map +1 -1
  68. package/dist/context-engine/auto-recall.js.map +1 -1
  69. package/dist/context-engine/compaction/clear-tool-results.d.ts +18 -3
  70. package/dist/context-engine/compaction/clear-tool-results.d.ts.map +1 -1
  71. package/dist/context-engine/compaction/clear-tool-results.js +26 -5
  72. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  73. package/dist/context-engine/compaction/compactor.js +19 -6
  74. package/dist/context-engine/compaction/compactor.js.map +1 -1
  75. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js +4 -0
  76. package/dist/context-engine/compaction/hooks/reanchor-persona-block.js.map +1 -1
  77. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js +1 -0
  78. package/dist/context-engine/compaction/hooks/reanchor-pinned-facts.js.map +1 -1
  79. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js +2 -1
  80. package/dist/context-engine/compaction/hooks/reanchor-project-rules.js.map +1 -1
  81. package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts +28 -0
  82. package/dist/context-engine/compaction/hooks/reanchor-recent-results.d.ts.map +1 -0
  83. package/dist/context-engine/compaction/hooks/reanchor-recent-results.js +71 -0
  84. package/dist/context-engine/compaction/hooks/reanchor-recent-results.js.map +1 -0
  85. package/dist/context-engine/compaction/hooks/types.d.ts +15 -3
  86. package/dist/context-engine/compaction/hooks/types.d.ts.map +1 -1
  87. package/dist/context-engine/compaction/index.d.ts +1 -0
  88. package/dist/context-engine/compaction/index.js +2 -1
  89. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +10 -3
  90. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  91. package/dist/context-engine/compaction/templates/summary-9-section.js +42 -5
  92. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  93. package/dist/context-engine/compaction/thresholds.js.map +1 -1
  94. package/dist/context-engine/compaction/types.d.ts +47 -7
  95. package/dist/context-engine/compaction/types.d.ts.map +1 -1
  96. package/dist/context-engine/engine.d.ts +23 -4
  97. package/dist/context-engine/engine.d.ts.map +1 -1
  98. package/dist/context-engine/engine.js +69 -15
  99. package/dist/context-engine/engine.js.map +1 -1
  100. package/dist/context-engine/index.d.ts +1 -0
  101. package/dist/context-engine/index.d.ts.map +1 -1
  102. package/dist/context-engine/index.js +2 -1
  103. package/dist/context-engine/locale-packs/en.js +14 -13
  104. package/dist/context-engine/locale-packs/en.js.map +1 -1
  105. package/dist/context-engine/locale-packs/types.d.ts +10 -10
  106. package/dist/context-engine/locale-packs/types.d.ts.map +1 -1
  107. package/dist/context-engine/locale-packs/types.js.map +1 -1
  108. package/dist/context-engine/metadata.d.ts +1 -1
  109. package/dist/context-engine/metadata.js +2 -2
  110. package/dist/context-engine/metadata.js.map +1 -1
  111. package/dist/context-engine/preambles/inbound-en.d.ts +1 -1
  112. package/dist/context-engine/preambles/inbound-en.js +1 -1
  113. package/dist/context-engine/preambles/inbound-en.js.map +1 -1
  114. package/dist/context-engine/privacy-filter.d.ts +2 -2
  115. package/dist/context-engine/privacy-filter.js +1 -1
  116. package/dist/context-engine/privacy-filter.js.map +1 -1
  117. package/dist/context-engine/templates/base-en.d.ts +1 -1
  118. package/dist/context-engine/templates/base-en.js +1 -1
  119. package/dist/context-engine/templates/base-en.js.map +1 -1
  120. package/dist/context-engine/templates/composer.d.ts +1 -1
  121. package/dist/context-engine/templates/composer.js.map +1 -1
  122. package/dist/context-engine/token-budget.d.ts +1 -1
  123. package/dist/context-engine/token-budget.js +1 -1
  124. package/dist/context-engine/token-budget.js.map +1 -1
  125. package/dist/context-engine/token-counter.d.ts +16 -7
  126. package/dist/context-engine/token-counter.d.ts.map +1 -1
  127. package/dist/context-engine/token-counter.js +31 -9
  128. package/dist/context-engine/token-counter.js.map +1 -1
  129. package/dist/errors/index.d.ts +2 -2
  130. package/dist/errors/index.js +4 -4
  131. package/dist/errors/index.js.map +1 -1
  132. package/dist/facade.d.ts +44 -15
  133. package/dist/facade.d.ts.map +1 -1
  134. package/dist/facade.js +16 -8
  135. package/dist/facade.js.map +1 -1
  136. package/dist/graph/entity-resolver.d.ts +2 -2
  137. package/dist/graph/entity-resolver.js +2 -2
  138. package/dist/graph/entity-resolver.js.map +1 -1
  139. package/dist/index.d.ts +9 -6
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +11 -5
  142. package/dist/index.js.map +1 -1
  143. package/dist/internal/contextualize.js +2 -2
  144. package/dist/internal/contextualize.js.map +1 -1
  145. package/dist/internal/embedder-binding.js +1 -1
  146. package/dist/internal/embedder-binding.js.map +1 -1
  147. package/dist/internal/id.js +1 -1
  148. package/dist/internal/id.js.map +1 -1
  149. package/dist/internal/injection-heuristics.js +25 -2
  150. package/dist/internal/injection-heuristics.js.map +1 -1
  151. package/dist/internal/spans.js +1 -1
  152. package/dist/internal/spans.js.map +1 -1
  153. package/dist/internal/storage-adapter.d.ts +71 -22
  154. package/dist/internal/storage-adapter.d.ts.map +1 -1
  155. package/dist/migration/embedder-migration.d.ts +7 -7
  156. package/dist/migration/embedder-migration.js.map +1 -1
  157. package/dist/package.js +6 -0
  158. package/dist/package.js.map +1 -0
  159. package/dist/search/explain.d.ts +7 -4
  160. package/dist/search/explain.d.ts.map +1 -1
  161. package/dist/search/explain.js +1 -1
  162. package/dist/search/explain.js.map +1 -1
  163. package/dist/search/fit-weights.d.ts +48 -0
  164. package/dist/search/fit-weights.d.ts.map +1 -0
  165. package/dist/search/fit-weights.js +70 -0
  166. package/dist/search/fit-weights.js.map +1 -0
  167. package/dist/search/graph-ppr.d.ts +36 -0
  168. package/dist/search/graph-ppr.d.ts.map +1 -0
  169. package/dist/search/graph-ppr.js +38 -0
  170. package/dist/search/graph-ppr.js.map +1 -0
  171. package/dist/search/index.d.ts +4 -1
  172. package/dist/search/index.js +4 -1
  173. package/dist/search/iterative.d.ts +15 -8
  174. package/dist/search/iterative.d.ts.map +1 -1
  175. package/dist/search/iterative.js +10 -9
  176. package/dist/search/iterative.js.map +1 -1
  177. package/dist/search/query-transform.d.ts +6 -6
  178. package/dist/search/query-transform.js +6 -6
  179. package/dist/search/query-transform.js.map +1 -1
  180. package/dist/search/rrf.d.ts +4 -4
  181. package/dist/search/rrf.js +4 -4
  182. package/dist/search/rrf.js.map +1 -1
  183. package/dist/search/trust.d.ts +30 -0
  184. package/dist/search/trust.d.ts.map +1 -0
  185. package/dist/search/trust.js +50 -0
  186. package/dist/search/trust.js.map +1 -0
  187. package/dist/search/types.d.ts +1 -1
  188. package/dist/tiers/episodic-memory.d.ts +6 -4
  189. package/dist/tiers/episodic-memory.d.ts.map +1 -1
  190. package/dist/tiers/episodic-memory.js +3 -2
  191. package/dist/tiers/episodic-memory.js.map +1 -1
  192. package/dist/tiers/index.d.ts +1 -1
  193. package/dist/tiers/index.js +1 -1
  194. package/dist/tiers/insight-memory.d.ts +4 -4
  195. package/dist/tiers/insight-memory.js +3 -3
  196. package/dist/tiers/insight-memory.js.map +1 -1
  197. package/dist/tiers/procedural-memory.d.ts +29 -7
  198. package/dist/tiers/procedural-memory.d.ts.map +1 -1
  199. package/dist/tiers/procedural-memory.js +66 -6
  200. package/dist/tiers/procedural-memory.js.map +1 -1
  201. package/dist/tiers/semantic-memory.d.ts +111 -30
  202. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  203. package/dist/tiers/semantic-memory.js +120 -27
  204. package/dist/tiers/semantic-memory.js.map +1 -1
  205. package/dist/tiers/session-memory.d.ts +7 -7
  206. package/dist/tiers/session-memory.js +7 -7
  207. package/dist/tiers/session-memory.js.map +1 -1
  208. package/dist/tiers/shared-memory.d.ts +1 -1
  209. package/dist/tiers/shared-memory.js +1 -1
  210. package/dist/tiers/shared-memory.js.map +1 -1
  211. package/dist/tiers/working-memory.d.ts +3 -3
  212. package/dist/tiers/working-memory.js +2 -2
  213. package/dist/tiers/working-memory.js.map +1 -1
  214. package/dist/tools/block-tools.d.ts +3 -3
  215. package/dist/tools/block-tools.js +3 -3
  216. package/dist/tools/block-tools.js.map +1 -1
  217. package/dist/tools/fact-tools.d.ts +48 -31
  218. package/dist/tools/fact-tools.d.ts.map +1 -1
  219. package/dist/tools/fact-tools.js +30 -18
  220. package/dist/tools/fact-tools.js.map +1 -1
  221. package/dist/tools/index.d.ts +9 -2
  222. package/dist/tools/index.d.ts.map +1 -1
  223. package/dist/tools/index.js +4 -2
  224. package/dist/tools/index.js.map +1 -1
  225. package/dist/tools/recall-tools.d.ts +40 -20
  226. package/dist/tools/recall-tools.d.ts.map +1 -1
  227. package/dist/tools/recall-tools.js +29 -10
  228. package/dist/tools/recall-tools.js.map +1 -1
  229. package/dist/tools/runbook-tools.d.ts +76 -0
  230. package/dist/tools/runbook-tools.d.ts.map +1 -0
  231. package/dist/tools/runbook-tools.js +53 -0
  232. package/dist/tools/runbook-tools.js.map +1 -0
  233. package/dist/tools/taint.js +33 -0
  234. package/dist/tools/taint.js.map +1 -0
  235. package/dist/tools/types.d.ts +1 -1
  236. package/package.json +8 -8
@@ -22,7 +22,7 @@ interface PrivacyFilterContext {
22
22
  readonly providerTrust?: LocalProviderTrust;
23
23
  /**
24
24
  * Per-user opt-in for cloud upload of `'internal'`-tier content.
25
- * Default `false` `'internal'` content does not leave the
25
+ * Default `false` - `'internal'` content does not leave the
26
26
  * loopback boundary unless the user explicitly opts in.
27
27
  */
28
28
  readonly cloudUploadConsent?: boolean;
@@ -57,7 +57,7 @@ type PrivacyDecisionReason = 'provider-rejects-secret' | 'provider-rejects-inter
57
57
  * - `'public-tls'` / `'public-mtls'` → `['public']`.
58
58
  * - `'public-cleartext'` → `['public']`.
59
59
  *
60
- * Per-provider overrides always win over the defaults pass
60
+ * Per-provider overrides always win over the defaults - pass
61
61
  * `providerAcceptsSensitivity` explicitly to override.
62
62
  *
63
63
  * @stable
@@ -11,7 +11,7 @@
11
11
  * - `'public-tls'` / `'public-mtls'` → `['public']`.
12
12
  * - `'public-cleartext'` → `['public']`.
13
13
  *
14
- * Per-provider overrides always win over the defaults pass
14
+ * Per-provider overrides always win over the defaults - pass
15
15
  * `providerAcceptsSensitivity` explicitly to override.
16
16
  *
17
17
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"privacy-filter.js","names":["kept: TRecord[]","dropped: Array<{ readonly record: TRecord; readonly reason: PrivacyDecisionReason }>","counters: Record<PrivacyDecisionReason, number>"],"sources":["../../src/context-engine/privacy-filter.ts"],"sourcesContent":["/**\n * D2 sensitivity-tier filter. Tags every memory record with a\n * decision (`'pass'` / `'drop'`) before the assembled prompt is\n * compiled. The filter is *tier-based* it trusts the\n * `record.sensitivity` tag the consolidator / writer set; the\n * outbound prompt-redaction middleware (D3, ADR-045) is the\n * universal backstop for content the filter cannot see (raw user\n * input, tool results, MCP responses, skill bodies, agent\n * instructions, tool-call args).\n *\n * @packageDocumentation\n */\n\nimport type { LocalProviderTrust, Sensitivity } from '@graphorin/core';\n\n/**\n * Filter input: the record-level sensitivity tag + the per-tier\n * trust class of the active provider.\n *\n * @stable\n */\nexport interface PrivacyFilterContext {\n /**\n * Sensitivity tiers the active provider is allowed to receive.\n * When omitted, the per-tier defaults below apply.\n */\n readonly providerAcceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Trust class of the active provider as classified by\n * `@graphorin/provider/trust/classify-local-provider.ts`. Defaults\n * to `'public-tls'` (the conservative cloud default).\n */\n readonly providerTrust?: LocalProviderTrust;\n /**\n * Per-user opt-in for cloud upload of `'internal'`-tier content.\n * Default `false` `'internal'` content does not leave the\n * loopback boundary unless the user explicitly opts in.\n */\n readonly cloudUploadConsent?: boolean;\n /**\n * Override the default sensitivity applied to records that are\n * missing a tag. Default `'internal'` per DEC-126.\n */\n readonly defaultSensitivity?: Sensitivity;\n}\n\n/**\n * Output of {@link decide} for a single record.\n *\n * @stable\n */\nexport type PrivacyDecision = 'pass' | 'drop';\n\n/**\n * Reason the decision was made. Surfaced to the metadata block so\n * operators can audit per-tier drops.\n *\n * @stable\n */\nexport type PrivacyDecisionReason =\n | 'provider-rejects-secret'\n | 'provider-rejects-internal'\n | 'no-cloud-upload-consent'\n | 'allowed';\n\n/**\n * Resolve the effective `acceptsSensitivity` set for a provider.\n * The function is pure: callers can supply a custom matrix to\n * override the defaults (used by tests + custom adapters).\n *\n * Defaults (DEC-149, ADR-013 ext):\n *\n * - `'loopback'` → `['public', 'internal', 'secret']`.\n * - `'private'` → `['public', 'internal']`.\n * - `'public-tls'` / `'public-mtls'` → `['public']`.\n * - `'public-cleartext'` → `['public']`.\n *\n * Per-provider overrides always win over the defaults pass\n * `providerAcceptsSensitivity` explicitly to override.\n *\n * @stable\n */\nexport function effectiveAcceptsSensitivity(\n context: PrivacyFilterContext,\n): ReadonlyArray<Sensitivity> {\n if (context.providerAcceptsSensitivity !== undefined) {\n return context.providerAcceptsSensitivity;\n }\n switch (context.providerTrust ?? 'public-tls') {\n case 'loopback':\n return ['public', 'internal', 'secret'];\n case 'private':\n return ['public', 'internal'];\n case 'public-tls':\n case 'public-cleartext':\n return ['public'];\n default:\n return ['public'];\n }\n}\n\n/**\n * Decide whether a single record is safe to send to the active\n * provider. The decision logic mirrors the DoD spec:\n *\n * - `'public'` always passes.\n * - `'secret'` passes only when the provider explicitly accepts\n * `'secret'` AND the trust class is `'loopback'`.\n * - `'internal'` passes when the provider accepts `'internal'`\n * AND `cloudUploadConsent === true` (or the trust class is\n * `'loopback'` / `'private'`).\n *\n * @stable\n */\nexport function decide(\n recordSensitivity: Sensitivity | undefined,\n context: PrivacyFilterContext,\n): { readonly decision: PrivacyDecision; readonly reason: PrivacyDecisionReason } {\n const sensitivity = recordSensitivity ?? context.defaultSensitivity ?? 'internal';\n const accepts = effectiveAcceptsSensitivity(context);\n const trust = context.providerTrust ?? 'public-tls';\n if (sensitivity === 'public') {\n return { decision: 'pass', reason: 'allowed' };\n }\n if (sensitivity === 'secret') {\n if (accepts.includes('secret') && trust === 'loopback') {\n return { decision: 'pass', reason: 'allowed' };\n }\n return { decision: 'drop', reason: 'provider-rejects-secret' };\n }\n // 'internal'\n if (!accepts.includes('internal')) {\n return { decision: 'drop', reason: 'provider-rejects-internal' };\n }\n if (trust === 'loopback' || trust === 'private') {\n return { decision: 'pass', reason: 'allowed' };\n }\n if (context.cloudUploadConsent === true) {\n return { decision: 'pass', reason: 'allowed' };\n }\n return { decision: 'drop', reason: 'no-cloud-upload-consent' };\n}\n\n/**\n * Bookkeeping returned by {@link partition}. Carries both the\n * surviving + dropped records and a per-reason counter for the\n * audit trail.\n *\n * @stable\n */\nexport interface PartitionResult<TRecord> {\n readonly kept: ReadonlyArray<TRecord>;\n readonly dropped: ReadonlyArray<{\n readonly record: TRecord;\n readonly reason: PrivacyDecisionReason;\n }>;\n readonly counters: Readonly<Record<PrivacyDecisionReason, number>>;\n}\n\n/**\n * Partition a record list against the supplied filter context.\n *\n * @stable\n */\nexport function partition<TRecord extends { readonly sensitivity?: Sensitivity }>(\n records: ReadonlyArray<TRecord>,\n context: PrivacyFilterContext,\n): PartitionResult<TRecord> {\n const kept: TRecord[] = [];\n const dropped: Array<{ readonly record: TRecord; readonly reason: PrivacyDecisionReason }> = [];\n const counters: Record<PrivacyDecisionReason, number> = {\n allowed: 0,\n 'no-cloud-upload-consent': 0,\n 'provider-rejects-internal': 0,\n 'provider-rejects-secret': 0,\n };\n for (const record of records) {\n const outcome = decide(record.sensitivity, context);\n counters[outcome.reason] += 1;\n if (outcome.decision === 'pass') {\n kept.push(record);\n } else {\n dropped.push({ record, reason: outcome.reason });\n }\n }\n return Object.freeze({\n kept: Object.freeze(kept),\n dropped: Object.freeze(dropped),\n counters: Object.freeze(counters),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkFA,SAAgB,4BACd,SAC4B;AAC5B,KAAI,QAAQ,+BAA+B,OACzC,QAAO,QAAQ;AAEjB,SAAQ,QAAQ,iBAAiB,cAAjC;EACE,KAAK,WACH,QAAO;GAAC;GAAU;GAAY;GAAS;EACzC,KAAK,UACH,QAAO,CAAC,UAAU,WAAW;EAC/B,KAAK;EACL,KAAK,mBACH,QAAO,CAAC,SAAS;EACnB,QACE,QAAO,CAAC,SAAS;;;;;;;;;;;;;;;;AAiBvB,SAAgB,OACd,mBACA,SACgF;CAChF,MAAM,cAAc,qBAAqB,QAAQ,sBAAsB;CACvE,MAAM,UAAU,4BAA4B,QAAQ;CACpD,MAAM,QAAQ,QAAQ,iBAAiB;AACvC,KAAI,gBAAgB,SAClB,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,KAAI,gBAAgB,UAAU;AAC5B,MAAI,QAAQ,SAAS,SAAS,IAAI,UAAU,WAC1C,QAAO;GAAE,UAAU;GAAQ,QAAQ;GAAW;AAEhD,SAAO;GAAE,UAAU;GAAQ,QAAQ;GAA2B;;AAGhE,KAAI,CAAC,QAAQ,SAAS,WAAW,CAC/B,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAA6B;AAElE,KAAI,UAAU,cAAc,UAAU,UACpC,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,KAAI,QAAQ,uBAAuB,KACjC,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAA2B;;;;;;;AAwBhE,SAAgB,UACd,SACA,SAC0B;CAC1B,MAAMA,OAAkB,EAAE;CAC1B,MAAMC,UAAuF,EAAE;CAC/F,MAAMC,WAAkD;EACtD,SAAS;EACT,2BAA2B;EAC3B,6BAA6B;EAC7B,2BAA2B;EAC5B;AACD,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,UAAU,OAAO,OAAO,aAAa,QAAQ;AACnD,WAAS,QAAQ,WAAW;AAC5B,MAAI,QAAQ,aAAa,OACvB,MAAK,KAAK,OAAO;MAEjB,SAAQ,KAAK;GAAE;GAAQ,QAAQ,QAAQ;GAAQ,CAAC;;AAGpD,QAAO,OAAO,OAAO;EACnB,MAAM,OAAO,OAAO,KAAK;EACzB,SAAS,OAAO,OAAO,QAAQ;EAC/B,UAAU,OAAO,OAAO,SAAS;EAClC,CAAC"}
1
+ {"version":3,"file":"privacy-filter.js","names":["kept: TRecord[]","dropped: Array<{ readonly record: TRecord; readonly reason: PrivacyDecisionReason }>","counters: Record<PrivacyDecisionReason, number>"],"sources":["../../src/context-engine/privacy-filter.ts"],"sourcesContent":["/**\n * D2 sensitivity-tier filter. Tags every memory record with a\n * decision (`'pass'` / `'drop'`) before the assembled prompt is\n * compiled. The filter is *tier-based* - it trusts the\n * `record.sensitivity` tag the consolidator / writer set; the\n * outbound prompt-redaction middleware (D3, ADR-045) is the\n * universal backstop for content the filter cannot see (raw user\n * input, tool results, MCP responses, skill bodies, agent\n * instructions, tool-call args).\n *\n * @packageDocumentation\n */\n\nimport type { LocalProviderTrust, Sensitivity } from '@graphorin/core';\n\n/**\n * Filter input: the record-level sensitivity tag + the per-tier\n * trust class of the active provider.\n *\n * @stable\n */\nexport interface PrivacyFilterContext {\n /**\n * Sensitivity tiers the active provider is allowed to receive.\n * When omitted, the per-tier defaults below apply.\n */\n readonly providerAcceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /**\n * Trust class of the active provider as classified by\n * `@graphorin/provider/trust/classify-local-provider.ts`. Defaults\n * to `'public-tls'` (the conservative cloud default).\n */\n readonly providerTrust?: LocalProviderTrust;\n /**\n * Per-user opt-in for cloud upload of `'internal'`-tier content.\n * Default `false` - `'internal'` content does not leave the\n * loopback boundary unless the user explicitly opts in.\n */\n readonly cloudUploadConsent?: boolean;\n /**\n * Override the default sensitivity applied to records that are\n * missing a tag. Default `'internal'` per DEC-126.\n */\n readonly defaultSensitivity?: Sensitivity;\n}\n\n/**\n * Output of {@link decide} for a single record.\n *\n * @stable\n */\nexport type PrivacyDecision = 'pass' | 'drop';\n\n/**\n * Reason the decision was made. Surfaced to the metadata block so\n * operators can audit per-tier drops.\n *\n * @stable\n */\nexport type PrivacyDecisionReason =\n | 'provider-rejects-secret'\n | 'provider-rejects-internal'\n | 'no-cloud-upload-consent'\n | 'allowed';\n\n/**\n * Resolve the effective `acceptsSensitivity` set for a provider.\n * The function is pure: callers can supply a custom matrix to\n * override the defaults (used by tests + custom adapters).\n *\n * Defaults (DEC-149, ADR-013 ext):\n *\n * - `'loopback'` → `['public', 'internal', 'secret']`.\n * - `'private'` → `['public', 'internal']`.\n * - `'public-tls'` / `'public-mtls'` → `['public']`.\n * - `'public-cleartext'` → `['public']`.\n *\n * Per-provider overrides always win over the defaults - pass\n * `providerAcceptsSensitivity` explicitly to override.\n *\n * @stable\n */\nexport function effectiveAcceptsSensitivity(\n context: PrivacyFilterContext,\n): ReadonlyArray<Sensitivity> {\n if (context.providerAcceptsSensitivity !== undefined) {\n return context.providerAcceptsSensitivity;\n }\n switch (context.providerTrust ?? 'public-tls') {\n case 'loopback':\n return ['public', 'internal', 'secret'];\n case 'private':\n return ['public', 'internal'];\n case 'public-tls':\n case 'public-cleartext':\n return ['public'];\n default:\n return ['public'];\n }\n}\n\n/**\n * Decide whether a single record is safe to send to the active\n * provider. The decision logic mirrors the DoD spec:\n *\n * - `'public'` always passes.\n * - `'secret'` passes only when the provider explicitly accepts\n * `'secret'` AND the trust class is `'loopback'`.\n * - `'internal'` passes when the provider accepts `'internal'`\n * AND `cloudUploadConsent === true` (or the trust class is\n * `'loopback'` / `'private'`).\n *\n * @stable\n */\nexport function decide(\n recordSensitivity: Sensitivity | undefined,\n context: PrivacyFilterContext,\n): { readonly decision: PrivacyDecision; readonly reason: PrivacyDecisionReason } {\n const sensitivity = recordSensitivity ?? context.defaultSensitivity ?? 'internal';\n const accepts = effectiveAcceptsSensitivity(context);\n const trust = context.providerTrust ?? 'public-tls';\n if (sensitivity === 'public') {\n return { decision: 'pass', reason: 'allowed' };\n }\n if (sensitivity === 'secret') {\n if (accepts.includes('secret') && trust === 'loopback') {\n return { decision: 'pass', reason: 'allowed' };\n }\n return { decision: 'drop', reason: 'provider-rejects-secret' };\n }\n // 'internal'\n if (!accepts.includes('internal')) {\n return { decision: 'drop', reason: 'provider-rejects-internal' };\n }\n if (trust === 'loopback' || trust === 'private') {\n return { decision: 'pass', reason: 'allowed' };\n }\n if (context.cloudUploadConsent === true) {\n return { decision: 'pass', reason: 'allowed' };\n }\n return { decision: 'drop', reason: 'no-cloud-upload-consent' };\n}\n\n/**\n * Bookkeeping returned by {@link partition}. Carries both the\n * surviving + dropped records and a per-reason counter for the\n * audit trail.\n *\n * @stable\n */\nexport interface PartitionResult<TRecord> {\n readonly kept: ReadonlyArray<TRecord>;\n readonly dropped: ReadonlyArray<{\n readonly record: TRecord;\n readonly reason: PrivacyDecisionReason;\n }>;\n readonly counters: Readonly<Record<PrivacyDecisionReason, number>>;\n}\n\n/**\n * Partition a record list against the supplied filter context.\n *\n * @stable\n */\nexport function partition<TRecord extends { readonly sensitivity?: Sensitivity }>(\n records: ReadonlyArray<TRecord>,\n context: PrivacyFilterContext,\n): PartitionResult<TRecord> {\n const kept: TRecord[] = [];\n const dropped: Array<{ readonly record: TRecord; readonly reason: PrivacyDecisionReason }> = [];\n const counters: Record<PrivacyDecisionReason, number> = {\n allowed: 0,\n 'no-cloud-upload-consent': 0,\n 'provider-rejects-internal': 0,\n 'provider-rejects-secret': 0,\n };\n for (const record of records) {\n const outcome = decide(record.sensitivity, context);\n counters[outcome.reason] += 1;\n if (outcome.decision === 'pass') {\n kept.push(record);\n } else {\n dropped.push({ record, reason: outcome.reason });\n }\n }\n return Object.freeze({\n kept: Object.freeze(kept),\n dropped: Object.freeze(dropped),\n counters: Object.freeze(counters),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkFA,SAAgB,4BACd,SAC4B;AAC5B,KAAI,QAAQ,+BAA+B,OACzC,QAAO,QAAQ;AAEjB,SAAQ,QAAQ,iBAAiB,cAAjC;EACE,KAAK,WACH,QAAO;GAAC;GAAU;GAAY;GAAS;EACzC,KAAK,UACH,QAAO,CAAC,UAAU,WAAW;EAC/B,KAAK;EACL,KAAK,mBACH,QAAO,CAAC,SAAS;EACnB,QACE,QAAO,CAAC,SAAS;;;;;;;;;;;;;;;;AAiBvB,SAAgB,OACd,mBACA,SACgF;CAChF,MAAM,cAAc,qBAAqB,QAAQ,sBAAsB;CACvE,MAAM,UAAU,4BAA4B,QAAQ;CACpD,MAAM,QAAQ,QAAQ,iBAAiB;AACvC,KAAI,gBAAgB,SAClB,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,KAAI,gBAAgB,UAAU;AAC5B,MAAI,QAAQ,SAAS,SAAS,IAAI,UAAU,WAC1C,QAAO;GAAE,UAAU;GAAQ,QAAQ;GAAW;AAEhD,SAAO;GAAE,UAAU;GAAQ,QAAQ;GAA2B;;AAGhE,KAAI,CAAC,QAAQ,SAAS,WAAW,CAC/B,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAA6B;AAElE,KAAI,UAAU,cAAc,UAAU,UACpC,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,KAAI,QAAQ,uBAAuB,KACjC,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAAW;AAEhD,QAAO;EAAE,UAAU;EAAQ,QAAQ;EAA2B;;;;;;;AAwBhE,SAAgB,UACd,SACA,SAC0B;CAC1B,MAAMA,OAAkB,EAAE;CAC1B,MAAMC,UAAuF,EAAE;CAC/F,MAAMC,WAAkD;EACtD,SAAS;EACT,2BAA2B;EAC3B,6BAA6B;EAC7B,2BAA2B;EAC5B;AACD,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,UAAU,OAAO,OAAO,aAAa,QAAQ;AACnD,WAAS,QAAQ,WAAW;AAC5B,MAAI,QAAQ,aAAa,OACvB,MAAK,KAAK,OAAO;MAEjB,SAAQ,KAAK;GAAE;GAAQ,QAAQ,QAAQ;GAAQ,CAAC;;AAGpD,QAAO,OAAO,OAAO;EACnB,MAAM,OAAO,OAAO,KAAK;EACzB,SAAS,OAAO,OAAO,QAAQ;EAC/B,UAAU,OAAO,OAAO,SAAS;EAClC,CAAC"}
@@ -4,7 +4,7 @@
4
4
  * fragments so call sites can import the bundled defaults without
5
5
  * dragging the full locale pack.
6
6
  *
7
- * The framework is locale-agnostic no language is privileged in
7
+ * The framework is locale-agnostic - no language is privileged in
8
8
  * core. Application code can register additional locales via
9
9
  * `defineContextLocalePack`.
10
10
  *
@@ -6,7 +6,7 @@ import { enLocalePack } from "../locale-packs/en.js";
6
6
  * fragments so call sites can import the bundled defaults without
7
7
  * dragging the full locale pack.
8
8
  *
9
- * The framework is locale-agnostic no language is privileged in
9
+ * The framework is locale-agnostic - no language is privileged in
10
10
  * core. Application code can register additional locales via
11
11
  * `defineContextLocalePack`.
12
12
  *
@@ -1 +1 @@
1
- {"version":3,"file":"base-en.js","names":[],"sources":["../../../src/context-engine/templates/base-en.ts"],"sourcesContent":["/**\n * Convenience re-export of the English Layer 1 base-template\n * fragments so call sites can import the bundled defaults without\n * dragging the full locale pack.\n *\n * The framework is locale-agnostic no language is privileged in\n * core. Application code can register additional locales via\n * `defineContextLocalePack`.\n *\n * @packageDocumentation\n */\n\nimport { enLocalePack } from '../locale-packs/en.js';\n\n/** English Layer 1 in `'full'` mode (~250-350 tokens). */\nexport const BASE_TEMPLATE_EN_FULL = enLocalePack.baseTemplate.full;\n\n/** English Layer 1 in `'minimal'` mode (~80-120 tokens). */\nexport const BASE_TEMPLATE_EN_MINIMAL = enLocalePack.baseTemplate.minimal;\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,wBAAwB,aAAa,aAAa;;AAG/D,MAAa,2BAA2B,aAAa,aAAa"}
1
+ {"version":3,"file":"base-en.js","names":[],"sources":["../../../src/context-engine/templates/base-en.ts"],"sourcesContent":["/**\n * Convenience re-export of the English Layer 1 base-template\n * fragments so call sites can import the bundled defaults without\n * dragging the full locale pack.\n *\n * The framework is locale-agnostic - no language is privileged in\n * core. Application code can register additional locales via\n * `defineContextLocalePack`.\n *\n * @packageDocumentation\n */\n\nimport { enLocalePack } from '../locale-packs/en.js';\n\n/** English Layer 1 in `'full'` mode (~250-350 tokens). */\nexport const BASE_TEMPLATE_EN_FULL = enLocalePack.baseTemplate.full;\n\n/** English Layer 1 in `'minimal'` mode (~80-120 tokens). */\nexport const BASE_TEMPLATE_EN_MINIMAL = enLocalePack.baseTemplate.minimal;\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAa,wBAAwB,aAAa,aAAa;;AAG/D,MAAa,2BAA2B,aAAa,aAAa"}
@@ -27,7 +27,7 @@ declare function composeLayer2(instructions: string | undefined): string;
27
27
  /**
28
28
  * Render Layer 4 skills metadata cards. Each entry is a
29
29
  * `<skill ... />` self-closing tag so the model sees only the
30
- * progressive-disclosure metadata the full SKILL.md body is
30
+ * progressive-disclosure metadata - the full SKILL.md body is
31
31
  * loaded on-demand via `activate_skill(name)` per RB-04.
32
32
  *
33
33
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"composer.js","names":[],"sources":["../../../src/context-engine/templates/composer.ts"],"sourcesContent":["/**\n * Layer 1 base-template composer. Resolves the locale pack, picks\n * the right base fragment based on `memoryBaseMode`, and exposes\n * the per-step inbound preamble as an additive Layer 5/6 fragment\n * (so the Layer 1-4 cache prefix stays bytes-equal across the run).\n *\n * @packageDocumentation\n */\n\nimport type { ContextLocalePack } from '../locale-packs/index.js';\n\n/**\n * Layer 1 mode discriminator. `'full'` ships the full educational\n * template (~250-350 tok); `'minimal'` ships the shortened version\n * (~80-120 tok) for top-tier models.\n *\n * @stable\n */\nexport type MemoryBaseMode = 'full' | 'minimal';\n\n/**\n * Compose Layer 1 from the resolved locale pack.\n *\n * @stable\n */\nexport function composeLayer1(pack: ContextLocalePack, mode: MemoryBaseMode): string {\n return mode === 'minimal' ? pack.baseTemplate.minimal : pack.baseTemplate.full;\n}\n\n/**\n * Render the optional Layer 2 (`agent_instructions`) wrapper.\n * Returns the empty string when `instructions` is empty so the\n * layer is dropped from the assembled prompt.\n *\n * @stable\n */\nexport function composeLayer2(instructions: string | undefined): string {\n if (instructions === undefined || instructions.trim().length === 0) return '';\n return `<agent_instructions>\\n${instructions}\\n</agent_instructions>`;\n}\n\n/**\n * Render Layer 4 skills metadata cards. Each entry is a\n * `<skill ... />` self-closing tag so the model sees only the\n * progressive-disclosure metadata the full SKILL.md body is\n * loaded on-demand via `activate_skill(name)` per RB-04.\n *\n * @stable\n */\nexport interface SkillMetadataCard {\n readonly name: string;\n readonly description: string;\n readonly location?: string;\n /** When `true`, the card is excluded from the assembled prompt. */\n readonly disableModelInvocation?: boolean;\n}\n\nexport function composeLayer4Skills(cards: ReadonlyArray<SkillMetadataCard>): string {\n const visible = cards.filter((c) => c.disableModelInvocation !== true);\n if (visible.length === 0) return '';\n const lines = ['<skills_available>'];\n for (const card of visible) {\n const description = ` description=\"${escapeXmlAttr(card.description)}\"`;\n const location =\n card.location !== undefined ? ` location=\"${escapeXmlAttr(card.location)}\"` : '';\n lines.push(` <skill name=\"${escapeXmlAttr(card.name)}\"${description}${location} />`);\n }\n lines.push('</skills_available>');\n return lines.join('\\n');\n}\n\n/**\n * Render the inbound-sanitization preamble fragment. Emitted\n * AFTER the cache breakpoint so the Layer 1-4 cache prefix is\n * unaffected. Caller threads the fragment into the system content\n * post-Layer 5; the fragment is returned as a string so the\n * caller does not need to know the cache-breakpoint policy.\n *\n * @stable\n */\nexport function composeInboundPreamble(pack: ContextLocalePack): string {\n return pack.inboundSanitizationPreamble.text;\n}\n\nfunction escapeXmlAttr(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n"],"mappings":";;;;;;AAyBA,SAAgB,cAAc,MAAyB,MAA8B;AACnF,QAAO,SAAS,YAAY,KAAK,aAAa,UAAU,KAAK,aAAa;;;;;;;;;AAU5E,SAAgB,cAAc,cAA0C;AACtE,KAAI,iBAAiB,UAAa,aAAa,MAAM,CAAC,WAAW,EAAG,QAAO;AAC3E,QAAO,yBAAyB,aAAa;;AAmB/C,SAAgB,oBAAoB,OAAiD;CACnF,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,2BAA2B,KAAK;AACtE,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAQ,CAAC,qBAAqB;AACpC,MAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,cAAc,iBAAiB,cAAc,KAAK,YAAY,CAAC;EACrE,MAAM,WACJ,KAAK,aAAa,SAAY,cAAc,cAAc,KAAK,SAAS,CAAC,KAAK;AAChF,QAAM,KAAK,kBAAkB,cAAc,KAAK,KAAK,CAAC,GAAG,cAAc,SAAS,KAAK;;AAEvF,OAAM,KAAK,sBAAsB;AACjC,QAAO,MAAM,KAAK,KAAK;;;;;;;;;;;AAYzB,SAAgB,uBAAuB,MAAiC;AACtE,QAAO,KAAK,4BAA4B;;AAG1C,SAAS,cAAc,OAAuB;AAC5C,QAAO,MACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS"}
1
+ {"version":3,"file":"composer.js","names":[],"sources":["../../../src/context-engine/templates/composer.ts"],"sourcesContent":["/**\n * Layer 1 base-template composer. Resolves the locale pack, picks\n * the right base fragment based on `memoryBaseMode`, and exposes\n * the per-step inbound preamble as an additive Layer 5/6 fragment\n * (so the Layer 1-4 cache prefix stays bytes-equal across the run).\n *\n * @packageDocumentation\n */\n\nimport type { ContextLocalePack } from '../locale-packs/index.js';\n\n/**\n * Layer 1 mode discriminator. `'full'` ships the full educational\n * template (~250-350 tok); `'minimal'` ships the shortened version\n * (~80-120 tok) for top-tier models.\n *\n * @stable\n */\nexport type MemoryBaseMode = 'full' | 'minimal';\n\n/**\n * Compose Layer 1 from the resolved locale pack.\n *\n * @stable\n */\nexport function composeLayer1(pack: ContextLocalePack, mode: MemoryBaseMode): string {\n return mode === 'minimal' ? pack.baseTemplate.minimal : pack.baseTemplate.full;\n}\n\n/**\n * Render the optional Layer 2 (`agent_instructions`) wrapper.\n * Returns the empty string when `instructions` is empty so the\n * layer is dropped from the assembled prompt.\n *\n * @stable\n */\nexport function composeLayer2(instructions: string | undefined): string {\n if (instructions === undefined || instructions.trim().length === 0) return '';\n return `<agent_instructions>\\n${instructions}\\n</agent_instructions>`;\n}\n\n/**\n * Render Layer 4 skills metadata cards. Each entry is a\n * `<skill ... />` self-closing tag so the model sees only the\n * progressive-disclosure metadata - the full SKILL.md body is\n * loaded on-demand via `activate_skill(name)` per RB-04.\n *\n * @stable\n */\nexport interface SkillMetadataCard {\n readonly name: string;\n readonly description: string;\n readonly location?: string;\n /** When `true`, the card is excluded from the assembled prompt. */\n readonly disableModelInvocation?: boolean;\n}\n\nexport function composeLayer4Skills(cards: ReadonlyArray<SkillMetadataCard>): string {\n const visible = cards.filter((c) => c.disableModelInvocation !== true);\n if (visible.length === 0) return '';\n const lines = ['<skills_available>'];\n for (const card of visible) {\n const description = ` description=\"${escapeXmlAttr(card.description)}\"`;\n const location =\n card.location !== undefined ? ` location=\"${escapeXmlAttr(card.location)}\"` : '';\n lines.push(` <skill name=\"${escapeXmlAttr(card.name)}\"${description}${location} />`);\n }\n lines.push('</skills_available>');\n return lines.join('\\n');\n}\n\n/**\n * Render the inbound-sanitization preamble fragment. Emitted\n * AFTER the cache breakpoint so the Layer 1-4 cache prefix is\n * unaffected. Caller threads the fragment into the system content\n * post-Layer 5; the fragment is returned as a string so the\n * caller does not need to know the cache-breakpoint policy.\n *\n * @stable\n */\nexport function composeInboundPreamble(pack: ContextLocalePack): string {\n return pack.inboundSanitizationPreamble.text;\n}\n\nfunction escapeXmlAttr(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n"],"mappings":";;;;;;AAyBA,SAAgB,cAAc,MAAyB,MAA8B;AACnF,QAAO,SAAS,YAAY,KAAK,aAAa,UAAU,KAAK,aAAa;;;;;;;;;AAU5E,SAAgB,cAAc,cAA0C;AACtE,KAAI,iBAAiB,UAAa,aAAa,MAAM,CAAC,WAAW,EAAG,QAAO;AAC3E,QAAO,yBAAyB,aAAa;;AAmB/C,SAAgB,oBAAoB,OAAiD;CACnF,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,2BAA2B,KAAK;AACtE,KAAI,QAAQ,WAAW,EAAG,QAAO;CACjC,MAAM,QAAQ,CAAC,qBAAqB;AACpC,MAAK,MAAM,QAAQ,SAAS;EAC1B,MAAM,cAAc,iBAAiB,cAAc,KAAK,YAAY,CAAC;EACrE,MAAM,WACJ,KAAK,aAAa,SAAY,cAAc,cAAc,KAAK,SAAS,CAAC,KAAK;AAChF,QAAM,KAAK,kBAAkB,cAAc,KAAK,KAAK,CAAC,GAAG,cAAc,SAAS,KAAK;;AAEvF,OAAM,KAAK,sBAAsB;AACjC,QAAO,MAAM,KAAK,KAAK;;;;;;;;;;;AAYzB,SAAgB,uBAAuB,MAAiC;AACtE,QAAO,KAAK,4BAA4B;;AAG1C,SAAS,cAAc,OAAuB;AAC5C,QAAO,MACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS"}
@@ -39,7 +39,7 @@ interface LayerCandidate {
39
39
  */
40
40
  type OverflowMode = 'truncate' | 'drop';
41
41
  /**
42
- * Output of {@link allocate} one entry per surviving layer in
42
+ * Output of {@link allocate} - one entry per surviving layer in
43
43
  * priority order, plus a `truncated` flag for observability.
44
44
  *
45
45
  * @stable
@@ -16,7 +16,7 @@ const DEFAULT_LAYER_PRIORITY = Object.freeze([
16
16
  /**
17
17
  * Tags opened-but-not-closed in `text`, innermost-first. A heuristic
18
18
  * scanner sized for the engine's own XML-ish layer markup
19
- * (`<memory_blocks>`, `<memory_rules>`, ...) self-closing tags are
19
+ * (`<memory_blocks>`, `<memory_rules>`, ...) - self-closing tags are
20
20
  * skipped and an unmatched closer is ignored.
21
21
  */
22
22
  function unclosedTags(text) {
@@ -1 +1 @@
1
- {"version":3,"file":"token-budget.js","names":["DEFAULT_LAYER_PRIORITY: ReadonlyArray<LayerId>","stack: string[]","capped: Array<{\n readonly candidate: LayerCandidate;\n text: string;\n tokens: number;\n truncated: boolean;\n }>","layersOut: LayerAllocation[]","overflowDropped: LayerId[]"],"sources":["../../src/context-engine/token-budget.ts"],"sourcesContent":["/**\n * Priority-ordered token-budget allocator. The allocator runs over the six\n * layers of the {@link ContextEngine}'s system prompt and trims the\n * lowest-priority layers first when the combined cost exceeds the configured\n * budget.\n *\n * This **truncation priority** is deliberately separate from the **emission\n * order** (CE-9): the engine emits the surviving layers in a stable order\n * (identity → rules → blocks → skills → [KV-cache breakpoint] → metadata →\n * auto-recall) regardless of which were trimmed, so the cacheable prefix stays\n * byte-stable across turns. This ladder governs only *what gets dropped first*.\n *\n * Priority (configurable), highest preserved → first truncated:\n *\n * 1. **identity** — Layer 1 (`graphorin_memory_base` + agent\n * instructions). Always preserved; truncated last.\n * 2. **memoryMetadata** — Layer 5. Small (~100-300 tokens).\n * 3. **activeRules** — Layer 4 procedural rules.\n * 4. **workingBlocks** — Layer 3.\n * 5. **activeSkills** — Layer 4 skills metadata cards.\n * 6. **autoRecall** — Layer 6. Lowest priority; truncated first.\n *\n * @packageDocumentation\n */\n\nimport type { ContextTokenCounter } from './token-counter.js';\n\n/**\n * Layer-id discriminator. Mirrors the documented Layer 1-6\n * structure of the layered system prompt.\n *\n * @stable\n */\nexport type LayerId =\n | 'identity'\n | 'memoryMetadata'\n | 'activeRules'\n | 'workingBlocks'\n | 'activeSkills'\n | 'autoRecall';\n\n/**\n * Default priority ladder. Higher priority means the layer is\n * preserved longer when the budget is tight.\n *\n * @stable\n */\nexport const DEFAULT_LAYER_PRIORITY: ReadonlyArray<LayerId> = Object.freeze([\n 'identity',\n 'memoryMetadata',\n 'activeRules',\n 'workingBlocks',\n 'activeSkills',\n 'autoRecall',\n]);\n\n/**\n * Single layer candidate fed to {@link allocate}. The `text` field\n * carries the rendered fragment; `cap` is the optional per-layer\n * upper bound (in tokens); `priority` overrides the default ladder\n * for advanced use cases.\n *\n * @stable\n */\nexport interface LayerCandidate {\n readonly id: LayerId;\n readonly text: string;\n readonly cap?: number;\n readonly priority?: number;\n}\n\n/**\n * Per-layer truncation mode used when the layer overflows its cap\n * or the global budget. `truncate` is the default; `drop` removes\n * the layer entirely when it would otherwise overflow.\n *\n * @stable\n */\nexport type OverflowMode = 'truncate' | 'drop';\n\n/**\n * Output of {@link allocate} — one entry per surviving layer in\n * priority order, plus a `truncated` flag for observability.\n *\n * @stable\n */\nexport interface LayerAllocation {\n readonly id: LayerId;\n readonly text: string;\n readonly tokens: number;\n readonly truncated: boolean;\n readonly droppedTokens: number;\n}\n\n/**\n * Result of an allocator run.\n *\n * @stable\n */\nexport interface AllocationResult {\n readonly layers: ReadonlyArray<LayerAllocation>;\n readonly totalTokens: number;\n readonly budgetTokens: number;\n readonly overflowDropped: ReadonlyArray<LayerId>;\n}\n\n/**\n * Tags opened-but-not-closed in `text`, innermost-first. A heuristic\n * scanner sized for the engine's own XML-ish layer markup\n * (`<memory_blocks>`, `<memory_rules>`, ...) — self-closing tags are\n * skipped and an unmatched closer is ignored.\n */\nfunction unclosedTags(text: string): string[] {\n const stack: string[] = [];\n const re = /<(\\/?)([A-Za-z_][\\w.-]*)(?:\\s[^<>]*)?(\\/?)>/g;\n let match = re.exec(text);\n while (match !== null) {\n const [, closing, name, selfClosing] = match;\n if (selfClosing !== '/' && name !== undefined) {\n if (closing === '/') {\n const idx = stack.lastIndexOf(name);\n if (idx !== -1) stack.splice(idx, 1);\n } else {\n stack.push(name);\n }\n }\n match = re.exec(text);\n }\n return stack.reverse();\n}\n\n/**\n * Snap a prefix cut to a structure-safe point (CE-16e): never end\n * inside a partially-emitted tag (`...<memory_blo`).\n */\nfunction snapCut(kept: string): string {\n const lastOpen = kept.lastIndexOf('<');\n return lastOpen > kept.lastIndexOf('>') ? kept.slice(0, lastOpen) : kept;\n}\n\n/**\n * Truncate `text` to fit `maxTokens`, preserving the leading\n * portion and replacing the trailing portion with the literal\n * `[...truncated]` marker. The token estimate is computed via the\n * supplied `counter`; truncation falls back to character-based\n * trimming when the estimate is non-monotonic.\n *\n * Structure-aware (CE-16e): the cut never splits a tag, and block\n * tags the cut leaves open are re-closed after the marker, so a\n * capped layer of XML-ish markup (e.g. `<memory_blocks>`) stays\n * well-formed in the assembled prompt. Plain strings are unaffected.\n *\n * @stable\n */\nexport async function truncateToTokens(\n text: string,\n maxTokens: number,\n counter: ContextTokenCounter,\n): Promise<{ readonly text: string; readonly tokens: number; readonly truncated: boolean }> {\n if (maxTokens <= 0) return { text: '', tokens: 0, truncated: text.length > 0 };\n const initial = await counter.countText(text);\n if (initial <= maxTokens) return { text, tokens: initial, truncated: false };\n const marker = '\\n[...truncated]';\n const markerTokens = await counter.countText(marker);\n const targetTokens = Math.max(0, maxTokens - markerTokens);\n if (targetTokens <= 0) return { text: marker, tokens: markerTokens, truncated: true };\n // Approximate the cut point via the inverse of the heuristic\n // chars-per-token ratio. Falls back to repeated halving when the\n // ratio is non-monotonic for the supplied counter.\n let lo = 0;\n let hi = text.length;\n let bestCut = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1;\n const slice = text.slice(0, mid);\n const tokens = await counter.countText(slice);\n if (tokens <= targetTokens) {\n bestCut = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n let kept = snapCut(text.slice(0, bestCut));\n let closers = unclosedTags(kept)\n .map((t) => `</${t}>`)\n .join('');\n let truncated = `${kept}${marker}${closers.length > 0 ? `\\n${closers}` : ''}`;\n let tokens = await counter.countText(truncated);\n // Re-closing may push past the cap — shave the kept body until the\n // whole thing (body + marker + closers) fits. Terminates: `kept`\n // shrinks by ≥1 char per pass, and the empty body fits by the\n // `targetTokens > 0` guard above.\n while (tokens > maxTokens && kept.length > 0) {\n kept = snapCut(kept.slice(0, kept.length - Math.ceil(kept.length * 0.1)));\n closers = unclosedTags(kept)\n .map((t) => `</${t}>`)\n .join('');\n truncated = `${kept}${marker}${closers.length > 0 ? `\\n${closers}` : ''}`;\n tokens = await counter.countText(truncated);\n }\n return { text: truncated, tokens, truncated: true };\n}\n\n/**\n * Run the allocator. Layers are sorted by priority ascending (the\n * first layer is the highest priority); when the running total\n * exceeds the budget, lower-priority layers are first capped at\n * their per-layer `cap` (when set) and finally truncated to\n * whatever fits.\n *\n * @stable\n */\nexport async function allocate(\n layers: ReadonlyArray<LayerCandidate>,\n budgetTokens: number,\n counter: ContextTokenCounter,\n options: {\n readonly priority?: ReadonlyArray<LayerId>;\n readonly overflowMode?: OverflowMode;\n } = {},\n): Promise<AllocationResult> {\n const priorityOrder = options.priority ?? DEFAULT_LAYER_PRIORITY;\n const overflowMode = options.overflowMode ?? 'truncate';\n const orderIndex = new Map<LayerId, number>();\n priorityOrder.forEach((id, idx) => {\n orderIndex.set(id, idx);\n });\n const sorted = [...layers].sort((a, b) => {\n const aPriority = a.priority ?? orderIndex.get(a.id) ?? 999;\n const bPriority = b.priority ?? orderIndex.get(b.id) ?? 999;\n return aPriority - bPriority;\n });\n\n // First pass: enforce per-layer caps (regardless of total budget).\n const capped: Array<{\n readonly candidate: LayerCandidate;\n text: string;\n tokens: number;\n truncated: boolean;\n }> = [];\n for (const candidate of sorted) {\n if (candidate.text.length === 0) {\n capped.push({ candidate, text: '', tokens: 0, truncated: false });\n continue;\n }\n if (candidate.cap !== undefined && candidate.cap >= 0) {\n const trimmed = await truncateToTokens(candidate.text, candidate.cap, counter);\n capped.push({\n candidate,\n text: trimmed.text,\n tokens: trimmed.tokens,\n truncated: trimmed.truncated,\n });\n } else {\n const tokens = await counter.countText(candidate.text);\n capped.push({ candidate, text: candidate.text, tokens, truncated: false });\n }\n }\n\n // Second pass: enforce the global budget. Walk the layers in\n // priority order, accumulating tokens; when the running total\n // exceeds the budget, the rest are dropped or truncated to fit.\n const budget = Math.max(0, budgetTokens);\n const layersOut: LayerAllocation[] = [];\n const overflowDropped: LayerId[] = [];\n let runningTotal = 0;\n for (const entry of capped) {\n if (entry.text.length === 0) {\n layersOut.push({\n id: entry.candidate.id,\n text: '',\n tokens: 0,\n truncated: false,\n droppedTokens: 0,\n });\n continue;\n }\n if (runningTotal + entry.tokens <= budget) {\n runningTotal += entry.tokens;\n layersOut.push({\n id: entry.candidate.id,\n text: entry.text,\n tokens: entry.tokens,\n truncated: entry.truncated,\n droppedTokens: 0,\n });\n continue;\n }\n const remaining = Math.max(0, budget - runningTotal);\n if (overflowMode === 'drop' || remaining === 0) {\n overflowDropped.push(entry.candidate.id);\n layersOut.push({\n id: entry.candidate.id,\n text: '',\n tokens: 0,\n truncated: false,\n droppedTokens: entry.tokens,\n });\n continue;\n }\n const trimmed = await truncateToTokens(entry.text, remaining, counter);\n runningTotal += trimmed.tokens;\n layersOut.push({\n id: entry.candidate.id,\n text: trimmed.text,\n tokens: trimmed.tokens,\n truncated: true,\n droppedTokens: entry.tokens - trimmed.tokens,\n });\n }\n\n return Object.freeze({\n layers: Object.freeze(layersOut),\n totalTokens: runningTotal,\n budgetTokens: budget,\n overflowDropped: Object.freeze(overflowDropped),\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"token-budget.js","names":["DEFAULT_LAYER_PRIORITY: ReadonlyArray<LayerId>","stack: string[]","capped: Array<{\n readonly candidate: LayerCandidate;\n text: string;\n tokens: number;\n truncated: boolean;\n }>","layersOut: LayerAllocation[]","overflowDropped: LayerId[]"],"sources":["../../src/context-engine/token-budget.ts"],"sourcesContent":["/**\n * Priority-ordered token-budget allocator. The allocator runs over the six\n * layers of the {@link ContextEngine}'s system prompt and trims the\n * lowest-priority layers first when the combined cost exceeds the configured\n * budget.\n *\n * This **truncation priority** is deliberately separate from the **emission\n * order** (CE-9): the engine emits the surviving layers in a stable order\n * (identity → rules → blocks → skills → [KV-cache breakpoint] → metadata →\n * auto-recall) regardless of which were trimmed, so the cacheable prefix stays\n * byte-stable across turns. This ladder governs only *what gets dropped first*.\n *\n * Priority (configurable), highest preserved → first truncated:\n *\n * 1. **identity** - Layer 1 (`graphorin_memory_base` + agent\n * instructions). Always preserved; truncated last.\n * 2. **memoryMetadata** - Layer 5. Small (~100-300 tokens).\n * 3. **activeRules** - Layer 4 procedural rules.\n * 4. **workingBlocks** - Layer 3.\n * 5. **activeSkills** - Layer 4 skills metadata cards.\n * 6. **autoRecall** - Layer 6. Lowest priority; truncated first.\n *\n * @packageDocumentation\n */\n\nimport type { ContextTokenCounter } from './token-counter.js';\n\n/**\n * Layer-id discriminator. Mirrors the documented Layer 1-6\n * structure of the layered system prompt.\n *\n * @stable\n */\nexport type LayerId =\n | 'identity'\n | 'memoryMetadata'\n | 'activeRules'\n | 'workingBlocks'\n | 'activeSkills'\n | 'autoRecall';\n\n/**\n * Default priority ladder. Higher priority means the layer is\n * preserved longer when the budget is tight.\n *\n * @stable\n */\nexport const DEFAULT_LAYER_PRIORITY: ReadonlyArray<LayerId> = Object.freeze([\n 'identity',\n 'memoryMetadata',\n 'activeRules',\n 'workingBlocks',\n 'activeSkills',\n 'autoRecall',\n]);\n\n/**\n * Single layer candidate fed to {@link allocate}. The `text` field\n * carries the rendered fragment; `cap` is the optional per-layer\n * upper bound (in tokens); `priority` overrides the default ladder\n * for advanced use cases.\n *\n * @stable\n */\nexport interface LayerCandidate {\n readonly id: LayerId;\n readonly text: string;\n readonly cap?: number;\n readonly priority?: number;\n}\n\n/**\n * Per-layer truncation mode used when the layer overflows its cap\n * or the global budget. `truncate` is the default; `drop` removes\n * the layer entirely when it would otherwise overflow.\n *\n * @stable\n */\nexport type OverflowMode = 'truncate' | 'drop';\n\n/**\n * Output of {@link allocate} - one entry per surviving layer in\n * priority order, plus a `truncated` flag for observability.\n *\n * @stable\n */\nexport interface LayerAllocation {\n readonly id: LayerId;\n readonly text: string;\n readonly tokens: number;\n readonly truncated: boolean;\n readonly droppedTokens: number;\n}\n\n/**\n * Result of an allocator run.\n *\n * @stable\n */\nexport interface AllocationResult {\n readonly layers: ReadonlyArray<LayerAllocation>;\n readonly totalTokens: number;\n readonly budgetTokens: number;\n readonly overflowDropped: ReadonlyArray<LayerId>;\n}\n\n/**\n * Tags opened-but-not-closed in `text`, innermost-first. A heuristic\n * scanner sized for the engine's own XML-ish layer markup\n * (`<memory_blocks>`, `<memory_rules>`, ...) - self-closing tags are\n * skipped and an unmatched closer is ignored.\n */\nfunction unclosedTags(text: string): string[] {\n const stack: string[] = [];\n const re = /<(\\/?)([A-Za-z_][\\w.-]*)(?:\\s[^<>]*)?(\\/?)>/g;\n let match = re.exec(text);\n while (match !== null) {\n const [, closing, name, selfClosing] = match;\n if (selfClosing !== '/' && name !== undefined) {\n if (closing === '/') {\n const idx = stack.lastIndexOf(name);\n if (idx !== -1) stack.splice(idx, 1);\n } else {\n stack.push(name);\n }\n }\n match = re.exec(text);\n }\n return stack.reverse();\n}\n\n/**\n * Snap a prefix cut to a structure-safe point (CE-16e): never end\n * inside a partially-emitted tag (`...<memory_blo`).\n */\nfunction snapCut(kept: string): string {\n const lastOpen = kept.lastIndexOf('<');\n return lastOpen > kept.lastIndexOf('>') ? kept.slice(0, lastOpen) : kept;\n}\n\n/**\n * Truncate `text` to fit `maxTokens`, preserving the leading\n * portion and replacing the trailing portion with the literal\n * `[...truncated]` marker. The token estimate is computed via the\n * supplied `counter`; truncation falls back to character-based\n * trimming when the estimate is non-monotonic.\n *\n * Structure-aware (CE-16e): the cut never splits a tag, and block\n * tags the cut leaves open are re-closed after the marker, so a\n * capped layer of XML-ish markup (e.g. `<memory_blocks>`) stays\n * well-formed in the assembled prompt. Plain strings are unaffected.\n *\n * @stable\n */\nexport async function truncateToTokens(\n text: string,\n maxTokens: number,\n counter: ContextTokenCounter,\n): Promise<{ readonly text: string; readonly tokens: number; readonly truncated: boolean }> {\n if (maxTokens <= 0) return { text: '', tokens: 0, truncated: text.length > 0 };\n const initial = await counter.countText(text);\n if (initial <= maxTokens) return { text, tokens: initial, truncated: false };\n const marker = '\\n[...truncated]';\n const markerTokens = await counter.countText(marker);\n const targetTokens = Math.max(0, maxTokens - markerTokens);\n if (targetTokens <= 0) return { text: marker, tokens: markerTokens, truncated: true };\n // Approximate the cut point via the inverse of the heuristic\n // chars-per-token ratio. Falls back to repeated halving when the\n // ratio is non-monotonic for the supplied counter.\n let lo = 0;\n let hi = text.length;\n let bestCut = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1;\n const slice = text.slice(0, mid);\n const tokens = await counter.countText(slice);\n if (tokens <= targetTokens) {\n bestCut = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n let kept = snapCut(text.slice(0, bestCut));\n let closers = unclosedTags(kept)\n .map((t) => `</${t}>`)\n .join('');\n let truncated = `${kept}${marker}${closers.length > 0 ? `\\n${closers}` : ''}`;\n let tokens = await counter.countText(truncated);\n // Re-closing may push past the cap - shave the kept body until the\n // whole thing (body + marker + closers) fits. Terminates: `kept`\n // shrinks by ≥1 char per pass, and the empty body fits by the\n // `targetTokens > 0` guard above.\n while (tokens > maxTokens && kept.length > 0) {\n kept = snapCut(kept.slice(0, kept.length - Math.ceil(kept.length * 0.1)));\n closers = unclosedTags(kept)\n .map((t) => `</${t}>`)\n .join('');\n truncated = `${kept}${marker}${closers.length > 0 ? `\\n${closers}` : ''}`;\n tokens = await counter.countText(truncated);\n }\n return { text: truncated, tokens, truncated: true };\n}\n\n/**\n * Run the allocator. Layers are sorted by priority ascending (the\n * first layer is the highest priority); when the running total\n * exceeds the budget, lower-priority layers are first capped at\n * their per-layer `cap` (when set) and finally truncated to\n * whatever fits.\n *\n * @stable\n */\nexport async function allocate(\n layers: ReadonlyArray<LayerCandidate>,\n budgetTokens: number,\n counter: ContextTokenCounter,\n options: {\n readonly priority?: ReadonlyArray<LayerId>;\n readonly overflowMode?: OverflowMode;\n } = {},\n): Promise<AllocationResult> {\n const priorityOrder = options.priority ?? DEFAULT_LAYER_PRIORITY;\n const overflowMode = options.overflowMode ?? 'truncate';\n const orderIndex = new Map<LayerId, number>();\n priorityOrder.forEach((id, idx) => {\n orderIndex.set(id, idx);\n });\n const sorted = [...layers].sort((a, b) => {\n const aPriority = a.priority ?? orderIndex.get(a.id) ?? 999;\n const bPriority = b.priority ?? orderIndex.get(b.id) ?? 999;\n return aPriority - bPriority;\n });\n\n // First pass: enforce per-layer caps (regardless of total budget).\n const capped: Array<{\n readonly candidate: LayerCandidate;\n text: string;\n tokens: number;\n truncated: boolean;\n }> = [];\n for (const candidate of sorted) {\n if (candidate.text.length === 0) {\n capped.push({ candidate, text: '', tokens: 0, truncated: false });\n continue;\n }\n if (candidate.cap !== undefined && candidate.cap >= 0) {\n const trimmed = await truncateToTokens(candidate.text, candidate.cap, counter);\n capped.push({\n candidate,\n text: trimmed.text,\n tokens: trimmed.tokens,\n truncated: trimmed.truncated,\n });\n } else {\n const tokens = await counter.countText(candidate.text);\n capped.push({ candidate, text: candidate.text, tokens, truncated: false });\n }\n }\n\n // Second pass: enforce the global budget. Walk the layers in\n // priority order, accumulating tokens; when the running total\n // exceeds the budget, the rest are dropped or truncated to fit.\n const budget = Math.max(0, budgetTokens);\n const layersOut: LayerAllocation[] = [];\n const overflowDropped: LayerId[] = [];\n let runningTotal = 0;\n for (const entry of capped) {\n if (entry.text.length === 0) {\n layersOut.push({\n id: entry.candidate.id,\n text: '',\n tokens: 0,\n truncated: false,\n droppedTokens: 0,\n });\n continue;\n }\n if (runningTotal + entry.tokens <= budget) {\n runningTotal += entry.tokens;\n layersOut.push({\n id: entry.candidate.id,\n text: entry.text,\n tokens: entry.tokens,\n truncated: entry.truncated,\n droppedTokens: 0,\n });\n continue;\n }\n const remaining = Math.max(0, budget - runningTotal);\n if (overflowMode === 'drop' || remaining === 0) {\n overflowDropped.push(entry.candidate.id);\n layersOut.push({\n id: entry.candidate.id,\n text: '',\n tokens: 0,\n truncated: false,\n droppedTokens: entry.tokens,\n });\n continue;\n }\n const trimmed = await truncateToTokens(entry.text, remaining, counter);\n runningTotal += trimmed.tokens;\n layersOut.push({\n id: entry.candidate.id,\n text: trimmed.text,\n tokens: trimmed.tokens,\n truncated: true,\n droppedTokens: entry.tokens - trimmed.tokens,\n });\n }\n\n return Object.freeze({\n layers: Object.freeze(layersOut),\n totalTokens: runningTotal,\n budgetTokens: budget,\n overflowDropped: Object.freeze(overflowDropped),\n });\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { Message, TokenCounter } from "@graphorin/core";
6
6
  * Pluggable text-token counter used inside the ContextEngine. The
7
7
  * surface is narrower than {@link TokenCounter} because the engine
8
8
  * never assembles a message list during the budget-allocation
9
- * phase it operates on rendered text fragments.
9
+ * phase - it operates on rendered text fragments.
10
10
  *
11
11
  * @stable
12
12
  */
@@ -15,7 +15,7 @@ interface ContextTokenCounter {
15
15
  countText(text: string): Promise<number>;
16
16
  }
17
17
  /**
18
- * Built-in heuristic counter chars/4 for Latin-ish text plus one
18
+ * Built-in heuristic counter - chars/4 for Latin-ish text plus one
19
19
  * token per dense-script (CJK/kana/hangul) character (CE-13). Stable
20
20
  * default when the operator does not pass a real {@link TokenCounter}.
21
21
  *
@@ -24,13 +24,17 @@ interface ContextTokenCounter {
24
24
  declare const HEURISTIC_TOKEN_COUNTER: ContextTokenCounter;
25
25
  /**
26
26
  * Wrap a real {@link TokenCounter} into the narrower
27
- * {@link ContextTokenCounter} surface. Calls `countText(text)`
28
- * directly for max precision; falls back to the synthetic
29
- * single-message bridge when only `count(messages)` is supported.
27
+ * {@link ContextTokenCounter} surface - PRESERVING the native
28
+ * message-level `count(messages)` (context-engine-03). The adapter
29
+ * used to keep only `countText`, which forced
30
+ * {@link countMessageTokens} onto the per-message render path for
31
+ * every real counter; combined with `renderMessageText` ignoring
32
+ * tool calls, tool-call arguments contributed zero to every trigger /
33
+ * before / after count while the provider billed for them.
30
34
  *
31
35
  * @stable
32
36
  */
33
- declare function adaptTokenCounter(counter: TokenCounter): ContextTokenCounter;
37
+ declare function adaptTokenCounter(counter: TokenCounter): ContextTokenCounter & Pick<TokenCounter, 'count'>;
34
38
  /**
35
39
  * Count tokens across a message list using a {@link TokenCounter}
36
40
  * (or the heuristic fallback). Used by the trigger-evaluation hot
@@ -44,7 +48,12 @@ declare function countMessageTokens(messages: ReadonlyArray<Message>, counter: T
44
48
  * Render a `Message` into a single textual approximation suitable
45
49
  * for token counting. Multimodal parts other than `'text'` /
46
50
  * `'reasoning'` contribute a constant approximation so the counter
47
- * does not silently under-count.
51
+ * does not silently under-count. Assistant tool calls render their
52
+ * name + serialized args (context-engine-03) - file writes and
53
+ * `code_execute` scripts are frequently the dominant tokens of an
54
+ * agentic step, and the provider serializes + counts them, so the
55
+ * engine's arithmetic must too (mirrors the provider-side
56
+ * `serialiseMessageForCount`).
48
57
  *
49
58
  * @stable
50
59
  */
@@ -1 +1 @@
1
- {"version":3,"file":"token-counter.d.ts","names":[],"sources":["../../src/context-engine/token-counter.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UA0BiB,mBAAA;;2BAEU;;;;;;;;;cAyBd,yBAAyB;;;;;;;;;iBAkBtB,iBAAA,UAA2B,eAAe;;;;;;;;;iBAkBpC,kBAAA,WACV,cAAc,mBACf,eAAe,sBACvB;;;;;;;;;iBAmBa,iBAAA,UAA2B"}
1
+ {"version":3,"file":"token-counter.d.ts","names":[],"sources":["../../src/context-engine/token-counter.ts"],"sourcesContent":[],"mappings":";;;;;;AA2HA;;;;;;UAjGiB,mBAAA;;2BAEU;;;;;;;;;cAyBd,yBAAyB;;;;;;;;;;;;;iBAsBtB,iBAAA,UACL,eACR,sBAAsB,KAAK;;;;;;;;;iBAmBR,kBAAA,WACV,cAAc,mBACf,eAAe,sBACvB;;;;;;;;;;;;;;iBAwBa,iBAAA,UAA2B"}
@@ -1,18 +1,18 @@
1
1
  //#region src/context-engine/token-counter.ts
2
2
  /**
3
3
  * Script-aware heuristic (CE-13). The flat chars/4 rule undercounts
4
- * non-Latin scripts ~4x real tokenizers emit roughly one token per
4
+ * non-Latin scripts ~4x - real tokenizers emit roughly one token per
5
5
  * CJK character (and per kana / hangul syllable), so a 200k-window
6
6
  * threshold expressed in "heuristic tokens" let CJK conversations blow
7
7
  * past the provider limit before compaction ever fired. Characters
8
8
  * outside the Latin-ish range count at one token each; Latin text keeps
9
- * the classic chars/4. Still a heuristic wire a real `TokenCounter`
9
+ * the classic chars/4. Still a heuristic - wire a real `TokenCounter`
10
10
  * (e.g. the provider package's JsTiktokenCounter) for production
11
11
  * accuracy.
12
12
  */
13
13
  const DENSE_SCRIPT_RE = /[ᄀ-ᇿ⺀-〿぀-ヿ㄰-㆏ㇰ-䶿一-鿿ꥠ-꥿가-퟿豈-﫿＀-￯]/g;
14
14
  /**
15
- * Built-in heuristic counter chars/4 for Latin-ish text plus one
15
+ * Built-in heuristic counter - chars/4 for Latin-ish text plus one
16
16
  * token per dense-script (CJK/kana/hangul) character (CE-13). Stable
17
17
  * default when the operator does not pass a real {@link TokenCounter}.
18
18
  *
@@ -29,15 +29,20 @@ const HEURISTIC_TOKEN_COUNTER = Object.freeze({
29
29
  });
30
30
  /**
31
31
  * Wrap a real {@link TokenCounter} into the narrower
32
- * {@link ContextTokenCounter} surface. Calls `countText(text)`
33
- * directly for max precision; falls back to the synthetic
34
- * single-message bridge when only `count(messages)` is supported.
32
+ * {@link ContextTokenCounter} surface - PRESERVING the native
33
+ * message-level `count(messages)` (context-engine-03). The adapter
34
+ * used to keep only `countText`, which forced
35
+ * {@link countMessageTokens} onto the per-message render path for
36
+ * every real counter; combined with `renderMessageText` ignoring
37
+ * tool calls, tool-call arguments contributed zero to every trigger /
38
+ * before / after count while the provider billed for them.
35
39
  *
36
40
  * @stable
37
41
  */
38
42
  function adaptTokenCounter(counter) {
39
43
  return Object.freeze({
40
44
  id: counter.id,
45
+ count: (messages) => counter.count(messages),
41
46
  async countText(text) {
42
47
  if (typeof text !== "string" || text.length === 0) return 0;
43
48
  return counter.countText(text);
@@ -62,19 +67,36 @@ async function countMessageTokens(messages, counter) {
62
67
  * Render a `Message` into a single textual approximation suitable
63
68
  * for token counting. Multimodal parts other than `'text'` /
64
69
  * `'reasoning'` contribute a constant approximation so the counter
65
- * does not silently under-count.
70
+ * does not silently under-count. Assistant tool calls render their
71
+ * name + serialized args (context-engine-03) - file writes and
72
+ * `code_execute` scripts are frequently the dominant tokens of an
73
+ * agentic step, and the provider serializes + counts them, so the
74
+ * engine's arithmetic must too (mirrors the provider-side
75
+ * `serialiseMessageForCount`).
66
76
  *
67
77
  * @stable
68
78
  */
69
79
  function renderMessageText(message) {
70
80
  if (message.role === "system") return message.content;
71
81
  const content = message.content;
72
- if (typeof content === "string") return content;
73
82
  let out = "";
74
- for (const part of content) if (part.type === "text" || part.type === "reasoning") out += `${part.text}\n`;
83
+ if (typeof content === "string") out = content;
84
+ else for (const part of content) if (part.type === "text" || part.type === "reasoning") out += `${part.text}\n`;
75
85
  else out += "[non-text-part]\n";
86
+ if (message.role === "assistant" && message.toolCalls !== void 0) for (const call of message.toolCalls) {
87
+ const args = typeof call.args === "string" ? call.args : safeStringifyArgs(call.args);
88
+ out += `\n[tool-call:${call.toolName}] ${args}`;
89
+ }
76
90
  return out;
77
91
  }
92
+ /** JSON-stringify tool args without ever throwing (circular → constant). */
93
+ function safeStringifyArgs(args) {
94
+ try {
95
+ return JSON.stringify(args) ?? "";
96
+ } catch {
97
+ return "[unserializable-args]";
98
+ }
99
+ }
78
100
 
79
101
  //#endregion
80
102
  export { HEURISTIC_TOKEN_COUNTER, adaptTokenCounter, countMessageTokens, renderMessageText };
@@ -1 +1 @@
1
- {"version":3,"file":"token-counter.js","names":["HEURISTIC_TOKEN_COUNTER: ContextTokenCounter"],"sources":["../../src/context-engine/token-counter.ts"],"sourcesContent":["/**\n * Token-counting helpers used by the {@link ContextEngine}. The\n * production path accepts a {@link TokenCounter} from\n * `@graphorin/core` (the default `JsTiktokenCounter` lives in\n * `@graphorin/provider`); the fallback path is a deterministic\n * heuristic that keeps unit tests dependency-free.\n *\n * The fallback counts ~1 token per 4 characters close enough for\n * budgeting decisions, intentionally less precise than a real BPE\n * tokenizer. Production deployments wire a real\n * {@link TokenCounter} through `createMemory({ contextEngine: {\n * tokenCounter: ... } })`.\n *\n * @packageDocumentation\n */\n\nimport type { Message, TokenCounter } from '@graphorin/core';\n\n/**\n * Pluggable text-token counter used inside the ContextEngine. The\n * surface is narrower than {@link TokenCounter} because the engine\n * never assembles a message list during the budget-allocation\n * phase it operates on rendered text fragments.\n *\n * @stable\n */\nexport interface ContextTokenCounter {\n readonly id: string;\n countText(text: string): Promise<number>;\n}\n\n/**\n * Script-aware heuristic (CE-13). The flat chars/4 rule undercounts\n * non-Latin scripts ~4x real tokenizers emit roughly one token per\n * CJK character (and per kana / hangul syllable), so a 200k-window\n * threshold expressed in \"heuristic tokens\" let CJK conversations blow\n * past the provider limit before compaction ever fired. Characters\n * outside the Latin-ish range count at one token each; Latin text keeps\n * the classic chars/4. Still a heuristic wire a real `TokenCounter`\n * (e.g. the provider package's JsTiktokenCounter) for production\n * accuracy.\n */\nconst DENSE_SCRIPT_RE =\n // CJK unified + extensions, kana, hangul, CJK punctuation, full-width forms.\n /[ᄀ-ᇿ⺀-〿぀-ヿ㄰-㆏ㇰ-䶿一-鿿ꥠ-꥿가-퟿豈-﫿＀-￯]/g;\n\n/**\n * Built-in heuristic counter chars/4 for Latin-ish text plus one\n * token per dense-script (CJK/kana/hangul) character (CE-13). Stable\n * default when the operator does not pass a real {@link TokenCounter}.\n *\n * @stable\n */\nexport const HEURISTIC_TOKEN_COUNTER: ContextTokenCounter = Object.freeze({\n id: 'graphorin/heuristic@chars-per-4+dense-script',\n async countText(text: string): Promise<number> {\n if (typeof text !== 'string' || text.length === 0) return 0;\n const dense = text.match(DENSE_SCRIPT_RE)?.length ?? 0;\n const latin = text.length - dense;\n return Math.ceil(latin / 4) + dense;\n },\n});\n\n/**\n * Wrap a real {@link TokenCounter} into the narrower\n * {@link ContextTokenCounter} surface. Calls `countText(text)`\n * directly for max precision; falls back to the synthetic\n * single-message bridge when only `count(messages)` is supported.\n *\n * @stable\n */\nexport function adaptTokenCounter(counter: TokenCounter): ContextTokenCounter {\n return Object.freeze({\n id: counter.id,\n async countText(text: string): Promise<number> {\n if (typeof text !== 'string' || text.length === 0) return 0;\n return counter.countText(text);\n },\n });\n}\n\n/**\n * Count tokens across a message list using a {@link TokenCounter}\n * (or the heuristic fallback). Used by the trigger-evaluation hot\n * path of the auto-compaction subsystem (RB-46) at the top of every\n * step.\n *\n * @stable\n */\nexport async function countMessageTokens(\n messages: ReadonlyArray<Message>,\n counter: TokenCounter | ContextTokenCounter,\n): Promise<number> {\n if ('count' in counter && typeof (counter as TokenCounter).count === 'function') {\n return (counter as TokenCounter).count(messages);\n }\n let total = 0;\n for (const message of messages) {\n total += await counter.countText(renderMessageText(message));\n }\n return total;\n}\n\n/**\n * Render a `Message` into a single textual approximation suitable\n * for token counting. Multimodal parts other than `'text'` /\n * `'reasoning'` contribute a constant approximation so the counter\n * does not silently under-count.\n *\n * @stable\n */\nexport function renderMessageText(message: Message): string {\n if (message.role === 'system') return message.content;\n const content = message.content;\n if (typeof content === 'string') return content;\n let out = '';\n for (const part of content) {\n if (part.type === 'text' || part.type === 'reasoning') {\n out += `${part.text}\\n`;\n } else {\n // Approximate non-text parts (image / audio / file) as a small\n // fixed cost so the counter does not silently under-count.\n out += '[non-text-part]\\n';\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,MAAM,kBAEJ;;;;;;;;AASF,MAAaA,0BAA+C,OAAO,OAAO;CACxE,IAAI;CACJ,MAAM,UAAU,MAA+B;AAC7C,MAAI,OAAO,SAAS,YAAY,KAAK,WAAW,EAAG,QAAO;EAC1D,MAAM,QAAQ,KAAK,MAAM,gBAAgB,EAAE,UAAU;EACrD,MAAM,QAAQ,KAAK,SAAS;AAC5B,SAAO,KAAK,KAAK,QAAQ,EAAE,GAAG;;CAEjC,CAAC;;;;;;;;;AAUF,SAAgB,kBAAkB,SAA4C;AAC5E,QAAO,OAAO,OAAO;EACnB,IAAI,QAAQ;EACZ,MAAM,UAAU,MAA+B;AAC7C,OAAI,OAAO,SAAS,YAAY,KAAK,WAAW,EAAG,QAAO;AAC1D,UAAO,QAAQ,UAAU,KAAK;;EAEjC,CAAC;;;;;;;;;;AAWJ,eAAsB,mBACpB,UACA,SACiB;AACjB,KAAI,WAAW,WAAW,OAAQ,QAAyB,UAAU,WACnE,QAAQ,QAAyB,MAAM,SAAS;CAElD,IAAI,QAAQ;AACZ,MAAK,MAAM,WAAW,SACpB,UAAS,MAAM,QAAQ,UAAU,kBAAkB,QAAQ,CAAC;AAE9D,QAAO;;;;;;;;;;AAWT,SAAgB,kBAAkB,SAA0B;AAC1D,KAAI,QAAQ,SAAS,SAAU,QAAO,QAAQ;CAC9C,MAAM,UAAU,QAAQ;AACxB,KAAI,OAAO,YAAY,SAAU,QAAO;CACxC,IAAI,MAAM;AACV,MAAK,MAAM,QAAQ,QACjB,KAAI,KAAK,SAAS,UAAU,KAAK,SAAS,YACxC,QAAO,GAAG,KAAK,KAAK;KAIpB,QAAO;AAGX,QAAO"}
1
+ {"version":3,"file":"token-counter.js","names":["HEURISTIC_TOKEN_COUNTER: ContextTokenCounter"],"sources":["../../src/context-engine/token-counter.ts"],"sourcesContent":["/**\n * Token-counting helpers used by the {@link ContextEngine}. The\n * production path accepts a {@link TokenCounter} from\n * `@graphorin/core` (the default `JsTiktokenCounter` lives in\n * `@graphorin/provider`); the fallback path is a deterministic\n * heuristic that keeps unit tests dependency-free.\n *\n * The fallback counts ~1 token per 4 characters - close enough for\n * budgeting decisions, intentionally less precise than a real BPE\n * tokenizer. Production deployments wire a real\n * {@link TokenCounter} through `createMemory({ contextEngine: {\n * tokenCounter: ... } })`.\n *\n * @packageDocumentation\n */\n\nimport type { Message, TokenCounter } from '@graphorin/core';\n\n/**\n * Pluggable text-token counter used inside the ContextEngine. The\n * surface is narrower than {@link TokenCounter} because the engine\n * never assembles a message list during the budget-allocation\n * phase - it operates on rendered text fragments.\n *\n * @stable\n */\nexport interface ContextTokenCounter {\n readonly id: string;\n countText(text: string): Promise<number>;\n}\n\n/**\n * Script-aware heuristic (CE-13). The flat chars/4 rule undercounts\n * non-Latin scripts ~4x - real tokenizers emit roughly one token per\n * CJK character (and per kana / hangul syllable), so a 200k-window\n * threshold expressed in \"heuristic tokens\" let CJK conversations blow\n * past the provider limit before compaction ever fired. Characters\n * outside the Latin-ish range count at one token each; Latin text keeps\n * the classic chars/4. Still a heuristic - wire a real `TokenCounter`\n * (e.g. the provider package's JsTiktokenCounter) for production\n * accuracy.\n */\nconst DENSE_SCRIPT_RE =\n // CJK unified + extensions, kana, hangul, CJK punctuation, full-width forms.\n /[ᄀ-ᇿ⺀-〿぀-ヿ㄰-㆏ㇰ-䶿一-鿿ꥠ-꥿가-퟿豈-﫿＀-￯]/g;\n\n/**\n * Built-in heuristic counter - chars/4 for Latin-ish text plus one\n * token per dense-script (CJK/kana/hangul) character (CE-13). Stable\n * default when the operator does not pass a real {@link TokenCounter}.\n *\n * @stable\n */\nexport const HEURISTIC_TOKEN_COUNTER: ContextTokenCounter = Object.freeze({\n id: 'graphorin/heuristic@chars-per-4+dense-script',\n async countText(text: string): Promise<number> {\n if (typeof text !== 'string' || text.length === 0) return 0;\n const dense = text.match(DENSE_SCRIPT_RE)?.length ?? 0;\n const latin = text.length - dense;\n return Math.ceil(latin / 4) + dense;\n },\n});\n\n/**\n * Wrap a real {@link TokenCounter} into the narrower\n * {@link ContextTokenCounter} surface - PRESERVING the native\n * message-level `count(messages)` (context-engine-03). The adapter\n * used to keep only `countText`, which forced\n * {@link countMessageTokens} onto the per-message render path for\n * every real counter; combined with `renderMessageText` ignoring\n * tool calls, tool-call arguments contributed zero to every trigger /\n * before / after count while the provider billed for them.\n *\n * @stable\n */\nexport function adaptTokenCounter(\n counter: TokenCounter,\n): ContextTokenCounter & Pick<TokenCounter, 'count'> {\n return Object.freeze({\n id: counter.id,\n count: (messages: ReadonlyArray<Message>): Promise<number> => counter.count(messages),\n async countText(text: string): Promise<number> {\n if (typeof text !== 'string' || text.length === 0) return 0;\n return counter.countText(text);\n },\n });\n}\n\n/**\n * Count tokens across a message list using a {@link TokenCounter}\n * (or the heuristic fallback). Used by the trigger-evaluation hot\n * path of the auto-compaction subsystem (RB-46) at the top of every\n * step.\n *\n * @stable\n */\nexport async function countMessageTokens(\n messages: ReadonlyArray<Message>,\n counter: TokenCounter | ContextTokenCounter,\n): Promise<number> {\n if ('count' in counter && typeof (counter as TokenCounter).count === 'function') {\n return (counter as TokenCounter).count(messages);\n }\n let total = 0;\n for (const message of messages) {\n total += await counter.countText(renderMessageText(message));\n }\n return total;\n}\n\n/**\n * Render a `Message` into a single textual approximation suitable\n * for token counting. Multimodal parts other than `'text'` /\n * `'reasoning'` contribute a constant approximation so the counter\n * does not silently under-count. Assistant tool calls render their\n * name + serialized args (context-engine-03) - file writes and\n * `code_execute` scripts are frequently the dominant tokens of an\n * agentic step, and the provider serializes + counts them, so the\n * engine's arithmetic must too (mirrors the provider-side\n * `serialiseMessageForCount`).\n *\n * @stable\n */\nexport function renderMessageText(message: Message): string {\n if (message.role === 'system') return message.content;\n const content = message.content;\n let out = '';\n if (typeof content === 'string') {\n out = content;\n } else {\n for (const part of content) {\n if (part.type === 'text' || part.type === 'reasoning') {\n out += `${part.text}\\n`;\n } else {\n // Approximate non-text parts (image / audio / file) as a small\n // fixed cost so the counter does not silently under-count.\n out += '[non-text-part]\\n';\n }\n }\n }\n if (message.role === 'assistant' && message.toolCalls !== undefined) {\n for (const call of message.toolCalls) {\n const args = typeof call.args === 'string' ? call.args : safeStringifyArgs(call.args);\n out += `\\n[tool-call:${call.toolName}] ${args}`;\n }\n }\n return out;\n}\n\n/** JSON-stringify tool args without ever throwing (circular → constant). */\nfunction safeStringifyArgs(args: unknown): string {\n try {\n return JSON.stringify(args) ?? '';\n } catch {\n return '[unserializable-args]';\n }\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,MAAM,kBAEJ;;;;;;;;AASF,MAAaA,0BAA+C,OAAO,OAAO;CACxE,IAAI;CACJ,MAAM,UAAU,MAA+B;AAC7C,MAAI,OAAO,SAAS,YAAY,KAAK,WAAW,EAAG,QAAO;EAC1D,MAAM,QAAQ,KAAK,MAAM,gBAAgB,EAAE,UAAU;EACrD,MAAM,QAAQ,KAAK,SAAS;AAC5B,SAAO,KAAK,KAAK,QAAQ,EAAE,GAAG;;CAEjC,CAAC;;;;;;;;;;;;;AAcF,SAAgB,kBACd,SACmD;AACnD,QAAO,OAAO,OAAO;EACnB,IAAI,QAAQ;EACZ,QAAQ,aAAsD,QAAQ,MAAM,SAAS;EACrF,MAAM,UAAU,MAA+B;AAC7C,OAAI,OAAO,SAAS,YAAY,KAAK,WAAW,EAAG,QAAO;AAC1D,UAAO,QAAQ,UAAU,KAAK;;EAEjC,CAAC;;;;;;;;;;AAWJ,eAAsB,mBACpB,UACA,SACiB;AACjB,KAAI,WAAW,WAAW,OAAQ,QAAyB,UAAU,WACnE,QAAQ,QAAyB,MAAM,SAAS;CAElD,IAAI,QAAQ;AACZ,MAAK,MAAM,WAAW,SACpB,UAAS,MAAM,QAAQ,UAAU,kBAAkB,QAAQ,CAAC;AAE9D,QAAO;;;;;;;;;;;;;;;AAgBT,SAAgB,kBAAkB,SAA0B;AAC1D,KAAI,QAAQ,SAAS,SAAU,QAAO,QAAQ;CAC9C,MAAM,UAAU,QAAQ;CACxB,IAAI,MAAM;AACV,KAAI,OAAO,YAAY,SACrB,OAAM;KAEN,MAAK,MAAM,QAAQ,QACjB,KAAI,KAAK,SAAS,UAAU,KAAK,SAAS,YACxC,QAAO,GAAG,KAAK,KAAK;KAIpB,QAAO;AAIb,KAAI,QAAQ,SAAS,eAAe,QAAQ,cAAc,OACxD,MAAK,MAAM,QAAQ,QAAQ,WAAW;EACpC,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,kBAAkB,KAAK,KAAK;AACrF,SAAO,gBAAgB,KAAK,SAAS,IAAI;;AAG7C,QAAO;;;AAIT,SAAS,kBAAkB,MAAuB;AAChD,KAAI;AACF,SAAO,KAAK,UAAU,KAAK,IAAI;SACzB;AACN,SAAO"}
@@ -81,7 +81,7 @@ declare class WorkingBlockReplaceMismatchError extends GraphorinMemoryError {
81
81
  /**
82
82
  * Thrown when any mutation targets a block defined with
83
83
  * `readOnly: true` (MRET-14). Previously this guard threw
84
- * `WorkingBlockReplaceMismatchError(label, 0)` semantically "your
84
+ * `WorkingBlockReplaceMismatchError(label, 0)` - semantically "your
85
85
  * unique substring matched 0 times", which misled callers that retry
86
86
  * replace operations on mismatch.
87
87
  *
@@ -153,7 +153,7 @@ declare class QuarantinePromotionRefusedError extends GraphorinMemoryError {
153
153
  /**
154
154
  * Raised when {@link ProceduralMemory.induce} (P2-2) is called but no
155
155
  * workflow inducer was configured. Induction abstracts concrete values into
156
- * variables, which needs a provider so the capability is opt-in and the
156
+ * variables, which needs a provider - so the capability is opt-in and the
157
157
  * default (offline) procedural tier never silently no-ops a requested
158
158
  * induction.
159
159
  *
@@ -96,7 +96,7 @@ var WorkingBlockReplaceMismatchError = class extends GraphorinMemoryError {
96
96
  /**
97
97
  * Thrown when any mutation targets a block defined with
98
98
  * `readOnly: true` (MRET-14). Previously this guard threw
99
- * `WorkingBlockReplaceMismatchError(label, 0)` semantically "your
99
+ * `WorkingBlockReplaceMismatchError(label, 0)` - semantically "your
100
100
  * unique substring matched 0 times", which misled callers that retry
101
101
  * replace operations on mismatch.
102
102
  *
@@ -107,7 +107,7 @@ var WorkingBlockReadOnlyError = class extends GraphorinMemoryError {
107
107
  kind = "working-block-read-only";
108
108
  label;
109
109
  constructor(label) {
110
- super(`[graphorin/memory] block '${label}' is read-only mutations are not allowed.`, { hint: "Define the block without readOnly, or write through an operator path that redefines it." });
110
+ super(`[graphorin/memory] block '${label}' is read-only - mutations are not allowed.`, { hint: "Define the block without readOnly, or write through an operator path that redefines it." });
111
111
  this.label = label;
112
112
  }
113
113
  };
@@ -176,7 +176,7 @@ var QuarantinePromotionRefusedError = class extends GraphorinMemoryError {
176
176
  /** Injection-rule labels that tripped on the fact's text. */
177
177
  markers;
178
178
  constructor(factId, markers) {
179
- super(`[graphorin/memory] refusing to promote fact '${factId}' out of quarantine: its text trips the injection heuristics (${markers.join(", ")}). Promotion of an injection-flagged memory is an operator action pass \`{ force: true }\` to validate(...) from a trusted (non-agent) caller after review.`, { hint: "Review the quarantined fact, then promote it with validate(scope, id, reason, { force: true }) from an operator context." });
179
+ super(`[graphorin/memory] refusing to promote fact '${factId}' out of quarantine: its text trips the injection heuristics (${markers.join(", ")}). Promotion of an injection-flagged memory is an operator action - pass \`{ force: true }\` to validate(...) from a trusted (non-agent) caller after review.`, { hint: "Review the quarantined fact, then promote it with validate(scope, id, reason, { force: true }) from an operator context." });
180
180
  this.factId = factId;
181
181
  this.markers = Object.freeze([...markers]);
182
182
  }
@@ -184,7 +184,7 @@ var QuarantinePromotionRefusedError = class extends GraphorinMemoryError {
184
184
  /**
185
185
  * Raised when {@link ProceduralMemory.induce} (P2-2) is called but no
186
186
  * workflow inducer was configured. Induction abstracts concrete values into
187
- * variables, which needs a provider so the capability is opt-in and the
187
+ * variables, which needs a provider - so the capability is opt-in and the
188
188
  * default (offline) procedural tier never silently no-ops a requested
189
189
  * induction.
190
190
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/errors/index.ts"],"sourcesContent":["/**\n * Typed error classes raised by `@graphorin/memory`. Every memory\n * subsystem throws one of these instead of plain `Error` so consumers\n * can pattern-match on `kind` and `name` without parsing the message.\n *\n * @packageDocumentation\n */\n\n/**\n * Base class for every error raised by `@graphorin/memory`. Carries a\n * stable lowercase `kind` discriminator and an optional `hint`\n * surfaced to operators (CLI command / docs link to fix the issue).\n *\n * @stable\n */\nexport abstract class GraphorinMemoryError extends Error {\n /** Stable lowercase discriminator. */\n abstract readonly kind: string;\n /** Optional actionable hint surfaced to operators. */\n readonly hint?: string;\n\n constructor(message: string, options?: { cause?: unknown; hint?: string }) {\n super(message, options !== undefined && 'cause' in options ? { cause: options.cause } : {});\n if (options?.hint !== undefined) {\n this.hint = options.hint;\n }\n }\n}\n\n/**\n * Raised when a memory write would reference an embedder that is not\n * registered in the storage layer's `embedding_meta` registry.\n *\n * @stable\n */\nexport class EmbedderRegistrationError extends GraphorinMemoryError {\n override readonly name = 'EmbedderRegistrationError';\n readonly kind = 'embedder-registration' as const;\n readonly embedderId: string;\n\n constructor(embedderId: string, options?: { cause?: unknown }) {\n super(\n `[graphorin/memory] embedder '${embedderId}' is not registered. ` +\n 'createMemory({ embedder, ... }) registers the configured embedder; ' +\n 'manual writes must reference an already-registered embedder_id.',\n {\n ...(options?.cause !== undefined ? { cause: options.cause } : {}),\n hint: 'Pass `embedder` to createMemory(...) so the registration runs at startup.',\n },\n );\n this.embedderId = embedderId;\n }\n}\n\n/**\n * Raised when a memory tool is invoked outside the per-tool ACL or the\n * memory-modification guard tier rejects the call.\n *\n * @stable\n */\nexport class MemoryToolDeniedError extends GraphorinMemoryError {\n override readonly name = 'MemoryToolDeniedError';\n readonly kind = 'memory-tool-denied' as const;\n readonly toolName: string;\n readonly reason: string;\n\n constructor(toolName: string, reason: string) {\n super(`[graphorin/memory] memory tool '${toolName}' was denied: ${reason}`, {\n hint: 'Check the tool authorisation matrix and the memory-modification guard tier policy.',\n });\n this.toolName = toolName;\n this.reason = reason;\n }\n}\n\n/**\n * Raised by `WorkingMemory` when a write would exceed the declared\n * `charLimit` and the overflow policy is `'reject'`.\n *\n * @stable\n */\nexport class WorkingBlockOverflowError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockOverflowError';\n readonly kind = 'working-block-overflow' as const;\n readonly label: string;\n readonly attempted: number;\n readonly charLimit: number;\n\n constructor(label: string, attempted: number, charLimit: number) {\n super(\n `[graphorin/memory] working block '${label}' overflow: attempted ${attempted} chars, limit is ${charLimit}.`,\n {\n hint: \"Switch the block's overflow policy to 'truncate' or shorten the input.\",\n },\n );\n this.label = label;\n this.attempted = attempted;\n this.charLimit = charLimit;\n }\n}\n\n/**\n * Raised by `WorkingMemory.replace(...)` when the supplied unique\n * substring is not present (or appears more than once) in the block\n * value.\n *\n * @stable\n */\nexport class WorkingBlockReplaceMismatchError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockReplaceMismatchError';\n readonly kind = 'working-block-replace-mismatch' as const;\n readonly label: string;\n readonly occurrences: number;\n\n constructor(label: string, occurrences: number) {\n super(\n `[graphorin/memory] block_replace on '${label}' expected exactly one occurrence of the unique substring, found ${occurrences}.`,\n {\n hint: 'Pass a longer unique substring or use block_rethink to replace the entire value.',\n },\n );\n this.label = label;\n this.occurrences = occurrences;\n }\n}\n\n/**\n * Thrown when any mutation targets a block defined with\n * `readOnly: true` (MRET-14). Previously this guard threw\n * `WorkingBlockReplaceMismatchError(label, 0)` — semantically \"your\n * unique substring matched 0 times\", which misled callers that retry\n * replace operations on mismatch.\n *\n * @stable\n */\nexport class WorkingBlockReadOnlyError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockReadOnlyError';\n readonly kind = 'working-block-read-only' as const;\n readonly label: string;\n\n constructor(label: string) {\n super(`[graphorin/memory] block '${label}' is read-only — mutations are not allowed.`, {\n hint: 'Define the block without readOnly, or write through an operator path that redefines it.',\n });\n this.label = label;\n }\n}\n\n/**\n * Raised by the embedder migration runner when the operator attempts a\n * silent embedder swap under the `lock-on-first` policy.\n *\n * @stable\n */\nexport class EmbedderMigrationLockedError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationLockedError';\n readonly kind = 'embedder-migration-locked' as const;\n readonly source: string;\n readonly target: string;\n\n constructor(source: string, target: string) {\n super(\n `[graphorin/memory] cannot swap embedder from '${source}' to '${target}' under the 'lock-on-first' policy. ` +\n 'Run `graphorin memory migrate` (Phase 15) or pass `policy: \"auto-migrate\"` / \"multi-active\".',\n {\n hint: 'Plan an explicit migration before swapping the active embedder.',\n },\n );\n this.source = source;\n this.target = target;\n }\n}\n\n/**\n * Raised by `migrateEmbedder(...)` when the runner is interrupted via\n * `AbortSignal`. The surrounding `for-await` loop receives this error so\n * the operator can re-run the migration. MST-12: there is no persisted\n * cursor today, so a re-run restarts from the beginning.\n *\n * @stable\n */\nexport class EmbedderMigrationAbortedError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationAbortedError';\n readonly kind = 'embedder-migration-aborted' as const;\n readonly migrationId: string;\n\n constructor(migrationId: string) {\n super(\n `[graphorin/memory] embedder migration '${migrationId}' aborted before completion. ` +\n 'Re-run `migrateEmbedder(...)` to restart (there is no persisted cursor yet, so it begins again).',\n {\n hint: 'Re-invoke migrateEmbedder(...) to restart the migration.',\n },\n );\n this.migrationId = migrationId;\n }\n}\n\n/**\n * Raised by the migration runner when the migration references embedders\n * that are no longer registered (e.g. the operator removed the underlying\n * tables manually).\n *\n * @stable\n */\nexport class EmbedderMigrationStateError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationStateError';\n readonly kind = 'embedder-migration-state' as const;\n\n constructor(message: string) {\n super(`[graphorin/memory] ${message}`, {\n hint: 'Re-register the source/target embedders (or re-run with the current ones) before retrying.',\n });\n }\n}\n\n/**\n * Raised by {@link SemanticMemory.validate} (P1-4 / MRET-3) when a caller\n * tries to promote a fact whose quarantine was triggered by the offline\n * injection heuristics. Such a fact is a memory-poisoning candidate: the\n * agent's own `fact_validate` tool must never be able to admit it into\n * action-driving recall, so promotion is refused unless an operator\n * passes the explicit `force` flag through the programmatic API.\n *\n * @stable\n */\nexport class QuarantinePromotionRefusedError extends GraphorinMemoryError {\n override readonly name = 'QuarantinePromotionRefusedError';\n readonly kind = 'quarantine-promotion-refused' as const;\n readonly factId: string;\n /** Injection-rule labels that tripped on the fact's text. */\n readonly markers: ReadonlyArray<string>;\n\n constructor(factId: string, markers: ReadonlyArray<string>) {\n super(\n `[graphorin/memory] refusing to promote fact '${factId}' out of quarantine: ` +\n `its text trips the injection heuristics (${markers.join(', ')}). ` +\n 'Promotion of an injection-flagged memory is an operator action — ' +\n 'pass `{ force: true }` to validate(...) from a trusted (non-agent) caller after review.',\n {\n hint: 'Review the quarantined fact, then promote it with validate(scope, id, reason, { force: true }) from an operator context.',\n },\n );\n this.factId = factId;\n this.markers = Object.freeze([...markers]);\n }\n}\n\n/**\n * Raised when {@link ProceduralMemory.induce} (P2-2) is called but no\n * workflow inducer was configured. Induction abstracts concrete values into\n * variables, which needs a provider — so the capability is opt-in and the\n * default (offline) procedural tier never silently no-ops a requested\n * induction.\n *\n * @stable\n */\nexport class ProcedureInductionNotConfiguredError extends GraphorinMemoryError {\n override readonly name = 'ProcedureInductionNotConfiguredError';\n readonly kind = 'procedure-induction-not-configured' as const;\n\n constructor(options?: { cause?: unknown }) {\n super(\n '[graphorin/memory] procedure induction requires a provider. ' +\n 'ProceduralMemory.induce(...) needs an inducer to abstract values into ' +\n 'variables; enable it with createMemory({ procedureInduction: { provider } }).',\n {\n ...(options?.cause !== undefined ? { cause: options.cause } : {}),\n hint: 'Pass `procedureInduction: { provider }` to createMemory(...).',\n },\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/errors/index.ts"],"sourcesContent":["/**\n * Typed error classes raised by `@graphorin/memory`. Every memory\n * subsystem throws one of these instead of plain `Error` so consumers\n * can pattern-match on `kind` and `name` without parsing the message.\n *\n * @packageDocumentation\n */\n\n/**\n * Base class for every error raised by `@graphorin/memory`. Carries a\n * stable lowercase `kind` discriminator and an optional `hint`\n * surfaced to operators (CLI command / docs link to fix the issue).\n *\n * @stable\n */\nexport abstract class GraphorinMemoryError extends Error {\n /** Stable lowercase discriminator. */\n abstract readonly kind: string;\n /** Optional actionable hint surfaced to operators. */\n readonly hint?: string;\n\n constructor(message: string, options?: { cause?: unknown; hint?: string }) {\n super(message, options !== undefined && 'cause' in options ? { cause: options.cause } : {});\n if (options?.hint !== undefined) {\n this.hint = options.hint;\n }\n }\n}\n\n/**\n * Raised when a memory write would reference an embedder that is not\n * registered in the storage layer's `embedding_meta` registry.\n *\n * @stable\n */\nexport class EmbedderRegistrationError extends GraphorinMemoryError {\n override readonly name = 'EmbedderRegistrationError';\n readonly kind = 'embedder-registration' as const;\n readonly embedderId: string;\n\n constructor(embedderId: string, options?: { cause?: unknown }) {\n super(\n `[graphorin/memory] embedder '${embedderId}' is not registered. ` +\n 'createMemory({ embedder, ... }) registers the configured embedder; ' +\n 'manual writes must reference an already-registered embedder_id.',\n {\n ...(options?.cause !== undefined ? { cause: options.cause } : {}),\n hint: 'Pass `embedder` to createMemory(...) so the registration runs at startup.',\n },\n );\n this.embedderId = embedderId;\n }\n}\n\n/**\n * Raised when a memory tool is invoked outside the per-tool ACL or the\n * memory-modification guard tier rejects the call.\n *\n * @stable\n */\nexport class MemoryToolDeniedError extends GraphorinMemoryError {\n override readonly name = 'MemoryToolDeniedError';\n readonly kind = 'memory-tool-denied' as const;\n readonly toolName: string;\n readonly reason: string;\n\n constructor(toolName: string, reason: string) {\n super(`[graphorin/memory] memory tool '${toolName}' was denied: ${reason}`, {\n hint: 'Check the tool authorisation matrix and the memory-modification guard tier policy.',\n });\n this.toolName = toolName;\n this.reason = reason;\n }\n}\n\n/**\n * Raised by `WorkingMemory` when a write would exceed the declared\n * `charLimit` and the overflow policy is `'reject'`.\n *\n * @stable\n */\nexport class WorkingBlockOverflowError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockOverflowError';\n readonly kind = 'working-block-overflow' as const;\n readonly label: string;\n readonly attempted: number;\n readonly charLimit: number;\n\n constructor(label: string, attempted: number, charLimit: number) {\n super(\n `[graphorin/memory] working block '${label}' overflow: attempted ${attempted} chars, limit is ${charLimit}.`,\n {\n hint: \"Switch the block's overflow policy to 'truncate' or shorten the input.\",\n },\n );\n this.label = label;\n this.attempted = attempted;\n this.charLimit = charLimit;\n }\n}\n\n/**\n * Raised by `WorkingMemory.replace(...)` when the supplied unique\n * substring is not present (or appears more than once) in the block\n * value.\n *\n * @stable\n */\nexport class WorkingBlockReplaceMismatchError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockReplaceMismatchError';\n readonly kind = 'working-block-replace-mismatch' as const;\n readonly label: string;\n readonly occurrences: number;\n\n constructor(label: string, occurrences: number) {\n super(\n `[graphorin/memory] block_replace on '${label}' expected exactly one occurrence of the unique substring, found ${occurrences}.`,\n {\n hint: 'Pass a longer unique substring or use block_rethink to replace the entire value.',\n },\n );\n this.label = label;\n this.occurrences = occurrences;\n }\n}\n\n/**\n * Thrown when any mutation targets a block defined with\n * `readOnly: true` (MRET-14). Previously this guard threw\n * `WorkingBlockReplaceMismatchError(label, 0)` - semantically \"your\n * unique substring matched 0 times\", which misled callers that retry\n * replace operations on mismatch.\n *\n * @stable\n */\nexport class WorkingBlockReadOnlyError extends GraphorinMemoryError {\n override readonly name = 'WorkingBlockReadOnlyError';\n readonly kind = 'working-block-read-only' as const;\n readonly label: string;\n\n constructor(label: string) {\n super(`[graphorin/memory] block '${label}' is read-only - mutations are not allowed.`, {\n hint: 'Define the block without readOnly, or write through an operator path that redefines it.',\n });\n this.label = label;\n }\n}\n\n/**\n * Raised by the embedder migration runner when the operator attempts a\n * silent embedder swap under the `lock-on-first` policy.\n *\n * @stable\n */\nexport class EmbedderMigrationLockedError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationLockedError';\n readonly kind = 'embedder-migration-locked' as const;\n readonly source: string;\n readonly target: string;\n\n constructor(source: string, target: string) {\n super(\n `[graphorin/memory] cannot swap embedder from '${source}' to '${target}' under the 'lock-on-first' policy. ` +\n 'Run `graphorin memory migrate` (Phase 15) or pass `policy: \"auto-migrate\"` / \"multi-active\".',\n {\n hint: 'Plan an explicit migration before swapping the active embedder.',\n },\n );\n this.source = source;\n this.target = target;\n }\n}\n\n/**\n * Raised by `migrateEmbedder(...)` when the runner is interrupted via\n * `AbortSignal`. The surrounding `for-await` loop receives this error so\n * the operator can re-run the migration. MST-12: there is no persisted\n * cursor today, so a re-run restarts from the beginning.\n *\n * @stable\n */\nexport class EmbedderMigrationAbortedError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationAbortedError';\n readonly kind = 'embedder-migration-aborted' as const;\n readonly migrationId: string;\n\n constructor(migrationId: string) {\n super(\n `[graphorin/memory] embedder migration '${migrationId}' aborted before completion. ` +\n 'Re-run `migrateEmbedder(...)` to restart (there is no persisted cursor yet, so it begins again).',\n {\n hint: 'Re-invoke migrateEmbedder(...) to restart the migration.',\n },\n );\n this.migrationId = migrationId;\n }\n}\n\n/**\n * Raised by the migration runner when the migration references embedders\n * that are no longer registered (e.g. the operator removed the underlying\n * tables manually).\n *\n * @stable\n */\nexport class EmbedderMigrationStateError extends GraphorinMemoryError {\n override readonly name = 'EmbedderMigrationStateError';\n readonly kind = 'embedder-migration-state' as const;\n\n constructor(message: string) {\n super(`[graphorin/memory] ${message}`, {\n hint: 'Re-register the source/target embedders (or re-run with the current ones) before retrying.',\n });\n }\n}\n\n/**\n * Raised by {@link SemanticMemory.validate} (P1-4 / MRET-3) when a caller\n * tries to promote a fact whose quarantine was triggered by the offline\n * injection heuristics. Such a fact is a memory-poisoning candidate: the\n * agent's own `fact_validate` tool must never be able to admit it into\n * action-driving recall, so promotion is refused unless an operator\n * passes the explicit `force` flag through the programmatic API.\n *\n * @stable\n */\nexport class QuarantinePromotionRefusedError extends GraphorinMemoryError {\n override readonly name = 'QuarantinePromotionRefusedError';\n readonly kind = 'quarantine-promotion-refused' as const;\n readonly factId: string;\n /** Injection-rule labels that tripped on the fact's text. */\n readonly markers: ReadonlyArray<string>;\n\n constructor(factId: string, markers: ReadonlyArray<string>) {\n super(\n `[graphorin/memory] refusing to promote fact '${factId}' out of quarantine: ` +\n `its text trips the injection heuristics (${markers.join(', ')}). ` +\n 'Promotion of an injection-flagged memory is an operator action - ' +\n 'pass `{ force: true }` to validate(...) from a trusted (non-agent) caller after review.',\n {\n hint: 'Review the quarantined fact, then promote it with validate(scope, id, reason, { force: true }) from an operator context.',\n },\n );\n this.factId = factId;\n this.markers = Object.freeze([...markers]);\n }\n}\n\n/**\n * Raised when {@link ProceduralMemory.induce} (P2-2) is called but no\n * workflow inducer was configured. Induction abstracts concrete values into\n * variables, which needs a provider - so the capability is opt-in and the\n * default (offline) procedural tier never silently no-ops a requested\n * induction.\n *\n * @stable\n */\nexport class ProcedureInductionNotConfiguredError extends GraphorinMemoryError {\n override readonly name = 'ProcedureInductionNotConfiguredError';\n readonly kind = 'procedure-induction-not-configured' as const;\n\n constructor(options?: { cause?: unknown }) {\n super(\n '[graphorin/memory] procedure induction requires a provider. ' +\n 'ProceduralMemory.induce(...) needs an inducer to abstract values into ' +\n 'variables; enable it with createMemory({ procedureInduction: { provider } }).',\n {\n ...(options?.cause !== undefined ? { cause: options.cause } : {}),\n hint: 'Pass `procedureInduction: { provider }` to createMemory(...).',\n },\n );\n }\n}\n"],"mappings":"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"}