@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
@@ -16,23 +16,23 @@ import { createConsolidator } from "./runtime.js";
16
16
 
17
17
  //#region src/consolidator/index.ts
18
18
  /**
19
- * `@graphorin/memory/consolidator` Phase 10c runtime that turns
19
+ * `@graphorin/memory/consolidator` - Phase 10c runtime that turns
20
20
  * raw conversation turns into long-lived facts and episodes. The
21
21
  * module ships:
22
22
  *
23
- * - {@link createConsolidator} the production factory that wires
23
+ * - {@link createConsolidator} - the production factory that wires
24
24
  * triggers, the budget envelope, the wait-then-defer lock, the
25
25
  * idempotent cursor, the DLQ, and the three phases (light /
26
26
  * standard / deep) into a single `Consolidator` handle.
27
- * - {@link createConsolidatorPlaceholder} kept for back-compat
27
+ * - {@link createConsolidatorPlaceholder} - kept for back-compat
28
28
  * with consumers that wired the no-op stub during Phase 10a; it
29
29
  * honours the same shape but does no background work.
30
30
  * - Pure helpers (`noise-filter`, `decay`, `budget`, `lock`, `dlq`,
31
- * `idempotency`, `triggers`) and the per-phase orchestrators
31
+ * `idempotency`, `triggers`) and the per-phase orchestrators -
32
32
  * exported so tests + the standalone server (Phase 14) can compose
33
33
  * them without re-implementing the algorithm.
34
34
  *
35
- * The `Consolidator` interface is **stable** Phase 14 wires the
35
+ * The `Consolidator` interface is **stable** - Phase 14 wires the
36
36
  * runtime into the lifecycle hooks, and Phase 15 surfaces every
37
37
  * method through `graphorin consolidator …` CLI commands.
38
38
  *
@@ -86,7 +86,9 @@ function createConsolidatorPlaceholder(options = {}) {
86
86
  reflection: false,
87
87
  importanceThreshold: 3,
88
88
  reflectionMaxQuestions: 3,
89
- contextualRetrieval: "late-chunk"
89
+ contextualRetrieval: "late-chunk",
90
+ learnedContext: false,
91
+ learnedContextMaxChars: 1200
90
92
  });
91
93
  const status = async () => Object.freeze({
92
94
  tier,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["tier: ConsolidatorTier","config: ConsolidatorConfig"],"sources":["../../src/consolidator/index.ts"],"sourcesContent":["/**\n * `@graphorin/memory/consolidator` Phase 10c runtime that turns\n * raw conversation turns into long-lived facts and episodes. The\n * module ships:\n *\n * - {@link createConsolidator} the production factory that wires\n * triggers, the budget envelope, the wait-then-defer lock, the\n * idempotent cursor, the DLQ, and the three phases (light /\n * standard / deep) into a single `Consolidator` handle.\n * - {@link createConsolidatorPlaceholder} kept for back-compat\n * with consumers that wired the no-op stub during Phase 10a; it\n * honours the same shape but does no background work.\n * - Pure helpers (`noise-filter`, `decay`, `budget`, `lock`, `dlq`,\n * `idempotency`, `triggers`) and the per-phase orchestrators —\n * exported so tests + the standalone server (Phase 14) can compose\n * them without re-implementing the algorithm.\n *\n * The `Consolidator` interface is **stable** Phase 14 wires the\n * runtime into the lifecycle hooks, and Phase 15 surfaces every\n * method through `graphorin consolidator …` CLI commands.\n *\n * @packageDocumentation\n */\n\nimport { DEFAULT_SALIENCE_WEIGHTS } from './decay.js';\nimport type { Consolidator } from './runtime.js';\nimport type {\n ConsolidatorConfig,\n ConsolidatorPhase,\n ConsolidatorStatus,\n ConsolidatorTier,\n ConsolidatorTriggerSpec,\n PhaseListener,\n PhaseOutcome,\n} from './types.js';\n\nexport {\n type BudgetCheck,\n type BudgetSnapshot,\n BudgetTracker,\n type BudgetTrackerOptions,\n bucketStart,\n nextBucketStart,\n} from './budget.js';\nexport {\n DEFAULT_DECAY_ARCHIVE_THRESHOLD,\n DEFAULT_DECAY_TAU_DAYS,\n DEFAULT_SALIENCE_WEIGHTS,\n NEUTRAL_IMPORTANCE,\n retention,\n type SalienceWeights,\n salience,\n selectForCapacityEviction,\n shouldArchive,\n} from './decay.js';\nexport {\n classifyError,\n describeError,\n nextBackoffMs,\n} from './dlq.js';\nexport {\n BudgetExceededError,\n CustomTierMisconfiguredError,\n ProviderNotConfiguredError,\n} from './errors.js';\nexport { tipMessageId } from './idempotency.js';\nexport { LockManager, type LockManagerOptions, type LockOutcome } from './lock.js';\nexport {\n applyNoiseFilters,\n type NoiseFilterPreset,\n type NoiseFilterResult,\n} from './noise-filter.js';\nexport { type DeepPhaseDeps, runDeepPhase } from './phases/deep.js';\nexport {\n buildInductionRequest,\n checkSuccessCriteria,\n createProviderWorkflowInducer,\n DEFAULT_INDUCTION_MAX_TOKENS,\n INDUCTION_SYSTEM_PROMPT,\n type InducedProcedure,\n MAX_PROCEDURE_STEPS,\n MAX_TRAJECTORY_STEPS_SHOWN,\n normalizeInducedProcedure,\n parseInducedProcedure,\n runWorkflowInduction,\n type Trajectory,\n type TrajectoryStep,\n trajectoryFromRunState,\n type VerificationResult,\n type WorkflowInducer,\n type WorkflowInductionOptions,\n} from './phases/induce.js';\nexport {\n type LightPhaseDeps,\n runLightPhase,\n} from './phases/light.js';\nexport {\n parseExtraction,\n runStandardPhase,\n type StandardPhaseDeps,\n} from './phases/standard.js';\nexport { type Consolidator, createConsolidator } from './runtime.js';\nexport {\n type ConsolidatorCatchupPolicy,\n type RegisterTriggersOptions,\n type RegisterTriggersResult,\n registerConsolidatorTriggers,\n type SchedulerLike,\n type TriggerDeclarationLike,\n} from './scheduler.js';\nexport {\n type ParsedTrigger,\n parseTriggerSpec,\n reasonFromTrigger,\n} from './triggers.js';\nexport type {\n ConsolidatorBudgetSnapshot,\n ConsolidatorCeilings,\n ConsolidatorConfig,\n ConsolidatorLastRuns,\n ConsolidatorPhase,\n ConsolidatorStatus,\n ConsolidatorTier,\n ConsolidatorTriggerReason,\n ConsolidatorTriggerSpec,\n CreateConsolidatorOptions,\n OnBudgetExceed,\n PhaseListener,\n PhaseOutcome,\n} from './types.js';\nexport { CONSOLIDATOR_TIER_DEFAULTS } from './types.js';\n\n/**\n * Build a no-op consolidator that honours the full {@link Consolidator}\n * interface but performs no background work. Useful for consumers\n * that want the typed shape (e.g., unit tests of higher tiers) but\n * do not pay the runtime cost of triggers / locking / DLQ.\n *\n * Phase 10c's {@link createConsolidator} is the production factory.\n *\n * @stable\n */\nexport function createConsolidatorPlaceholder(\n options: {\n readonly triggers?: ReadonlyArray<ConsolidatorTriggerSpec>;\n readonly tier?: ConsolidatorTier;\n } = {},\n): Consolidator {\n let running = false;\n let paused = false;\n let tier: ConsolidatorTier = options.tier ?? 'free';\n const triggers = Object.freeze([\n // Matches the real runtime default (MCON-4): idle drives light/standard,\n // the daily cron makes the deep phase reachable; turn:N is consumer-emitted\n // so it is not a scheduler default.\n ...(options.triggers ?? (['idle:5m', 'cron:0 4 * * *'] as ConsolidatorTriggerSpec[])),\n ]) as ReadonlyArray<ConsolidatorTriggerSpec>;\n const listeners = new Set<PhaseListener>();\n\n const config: ConsolidatorConfig = Object.freeze({\n triggers,\n tier,\n phases: Object.freeze(['light' as ConsolidatorPhase]),\n ceilings: {\n maxTokensPerDay: 0,\n maxCostPerDay: 0,\n maxConcurrentRuns: 1,\n maxRunDurationMs: 5 * 60 * 1000,\n cooldownMs: 60_000,\n },\n onExceed: 'pause',\n cheapModel: null,\n deepModel: null,\n budgetResetSemantics: 'utc',\n noiseFilters: Object.freeze(['default' as const]),\n lockWaitMs: 30_000,\n decayTauDays: 7,\n decayArchiveThreshold: 0.05,\n decayCapacity: null,\n salienceWeights: DEFAULT_SALIENCE_WEIGHTS,\n maxStandardBatchSize: 50,\n maxDeepConflictsPerRun: 20,\n dlqMaxRetries: 5,\n dlqBaseBackoffMs: 60_000,\n dlqMaxBackoffMs: 60 * 60 * 1000,\n formEpisodes: false,\n importanceScoring: false,\n autoPromoteExtraction: false,\n reflection: false,\n importanceThreshold: 3,\n reflectionMaxQuestions: 3,\n contextualRetrieval: 'late-chunk',\n });\n\n const status = async (): Promise<ConsolidatorStatus> =>\n Object.freeze({\n tier,\n triggers,\n phases: config.phases,\n running,\n paused,\n pendingConflicts: 0,\n queueDepth: 0,\n dlqSize: 0,\n deferredRuns: 0,\n emptyExtractions: 0,\n lastRuns: Object.freeze({}),\n budget: {\n tokensUsedToday: 0,\n costUsedToday: 0,\n tokensRemaining: 0,\n costRemaining: 0,\n resetAt: new Date(Date.now() + 86_400_000).toISOString(),\n },\n budgetRemaining: {\n tokens: 0,\n costUsd: 0,\n },\n });\n\n return {\n async start() {\n running = true;\n },\n async stop() {\n running = false;\n },\n async trigger() {\n return null;\n },\n async fireNow(): Promise<PhaseOutcome | null> {\n return null;\n },\n async setTier(next: ConsolidatorTier) {\n tier = next;\n },\n async pause() {\n paused = true;\n },\n async resume() {\n paused = false;\n },\n recordExternalSpend(): void {\n // Placeholder no budget to record against.\n },\n onPhaseFinished(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n config() {\n return config;\n },\n async registerWithScheduler() {\n // No-op consolidator: nothing to schedule.\n return Object.freeze({ registered: Object.freeze([]), skipped: Object.freeze([]) });\n },\n isFree() {\n return tier === 'free';\n },\n async drainDlq() {\n return 0;\n },\n status,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8IA,SAAgB,8BACd,UAGI,EAAE,EACQ;CACd,IAAI,UAAU;CACd,IAAI,SAAS;CACb,IAAIA,OAAyB,QAAQ,QAAQ;CAC7C,MAAM,WAAW,OAAO,OAAO,CAI7B,GAAI,QAAQ,YAAa,CAAC,WAAW,iBAAiB,CACvD,CAAC;CACF,MAAM,4BAAY,IAAI,KAAoB;CAE1C,MAAMC,SAA6B,OAAO,OAAO;EAC/C;EACA;EACA,QAAQ,OAAO,OAAO,CAAC,QAA6B,CAAC;EACrD,UAAU;GACR,iBAAiB;GACjB,eAAe;GACf,mBAAmB;GACnB,kBAAkB,MAAS;GAC3B,YAAY;GACb;EACD,UAAU;EACV,YAAY;EACZ,WAAW;EACX,sBAAsB;EACtB,cAAc,OAAO,OAAO,CAAC,UAAmB,CAAC;EACjD,YAAY;EACZ,cAAc;EACd,uBAAuB;EACvB,eAAe;EACf,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,eAAe;EACf,kBAAkB;EAClB,iBAAiB,OAAU;EAC3B,cAAc;EACd,mBAAmB;EACnB,uBAAuB;EACvB,YAAY;EACZ,qBAAqB;EACrB,wBAAwB;EACxB,qBAAqB;EACtB,CAAC;CAEF,MAAM,SAAS,YACb,OAAO,OAAO;EACZ;EACA;EACA,QAAQ,OAAO;EACf;EACA;EACA,kBAAkB;EAClB,YAAY;EACZ,SAAS;EACT,cAAc;EACd,kBAAkB;EAClB,UAAU,OAAO,OAAO,EAAE,CAAC;EAC3B,QAAQ;GACN,iBAAiB;GACjB,eAAe;GACf,iBAAiB;GACjB,eAAe;GACf,SAAS,IAAI,KAAK,KAAK,KAAK,GAAG,MAAW,CAAC,aAAa;GACzD;EACD,iBAAiB;GACf,QAAQ;GACR,SAAS;GACV;EACF,CAAC;AAEJ,QAAO;EACL,MAAM,QAAQ;AACZ,aAAU;;EAEZ,MAAM,OAAO;AACX,aAAU;;EAEZ,MAAM,UAAU;AACd,UAAO;;EAET,MAAM,UAAwC;AAC5C,UAAO;;EAET,MAAM,QAAQ,MAAwB;AACpC,UAAO;;EAET,MAAM,QAAQ;AACZ,YAAS;;EAEX,MAAM,SAAS;AACb,YAAS;;EAEX,sBAA4B;EAG5B,gBAAgB,UAAU;AACxB,aAAU,IAAI,SAAS;AACvB,gBAAa,UAAU,OAAO,SAAS;;EAEzC,SAAS;AACP,UAAO;;EAET,MAAM,wBAAwB;AAE5B,UAAO,OAAO,OAAO;IAAE,YAAY,OAAO,OAAO,EAAE,CAAC;IAAE,SAAS,OAAO,OAAO,EAAE,CAAC;IAAE,CAAC;;EAErF,SAAS;AACP,UAAO,SAAS;;EAElB,MAAM,WAAW;AACf,UAAO;;EAET;EACD"}
1
+ {"version":3,"file":"index.js","names":["tier: ConsolidatorTier","config: ConsolidatorConfig"],"sources":["../../src/consolidator/index.ts"],"sourcesContent":["/**\n * `@graphorin/memory/consolidator` - Phase 10c runtime that turns\n * raw conversation turns into long-lived facts and episodes. The\n * module ships:\n *\n * - {@link createConsolidator} - the production factory that wires\n * triggers, the budget envelope, the wait-then-defer lock, the\n * idempotent cursor, the DLQ, and the three phases (light /\n * standard / deep) into a single `Consolidator` handle.\n * - {@link createConsolidatorPlaceholder} - kept for back-compat\n * with consumers that wired the no-op stub during Phase 10a; it\n * honours the same shape but does no background work.\n * - Pure helpers (`noise-filter`, `decay`, `budget`, `lock`, `dlq`,\n * `idempotency`, `triggers`) and the per-phase orchestrators -\n * exported so tests + the standalone server (Phase 14) can compose\n * them without re-implementing the algorithm.\n *\n * The `Consolidator` interface is **stable** - Phase 14 wires the\n * runtime into the lifecycle hooks, and Phase 15 surfaces every\n * method through `graphorin consolidator …` CLI commands.\n *\n * @packageDocumentation\n */\n\nimport { DEFAULT_SALIENCE_WEIGHTS } from './decay.js';\nimport type { Consolidator } from './runtime.js';\nimport type {\n ConsolidatorConfig,\n ConsolidatorPhase,\n ConsolidatorStatus,\n ConsolidatorTier,\n ConsolidatorTriggerSpec,\n PhaseListener,\n PhaseOutcome,\n} from './types.js';\n\nexport {\n type BudgetCheck,\n type BudgetSnapshot,\n BudgetTracker,\n type BudgetTrackerOptions,\n bucketStart,\n nextBucketStart,\n} from './budget.js';\nexport {\n DEFAULT_DECAY_ARCHIVE_THRESHOLD,\n DEFAULT_DECAY_TAU_DAYS,\n DEFAULT_SALIENCE_WEIGHTS,\n NEUTRAL_IMPORTANCE,\n retention,\n type SalienceWeights,\n salience,\n selectForCapacityEviction,\n shouldArchive,\n} from './decay.js';\nexport {\n classifyError,\n describeError,\n nextBackoffMs,\n} from './dlq.js';\nexport {\n BudgetExceededError,\n CustomTierMisconfiguredError,\n ProviderNotConfiguredError,\n} from './errors.js';\nexport { tipMessageId } from './idempotency.js';\nexport { LockManager, type LockManagerOptions, type LockOutcome } from './lock.js';\nexport {\n applyNoiseFilters,\n type NoiseFilterPreset,\n type NoiseFilterResult,\n} from './noise-filter.js';\nexport { type DeepPhaseDeps, runDeepPhase } from './phases/deep.js';\nexport {\n buildInductionRequest,\n checkSuccessCriteria,\n createProviderWorkflowInducer,\n DEFAULT_INDUCTION_MAX_TOKENS,\n INDUCTION_SYSTEM_PROMPT,\n type InducedProcedure,\n MAX_PROCEDURE_STEPS,\n MAX_TRAJECTORY_STEPS_SHOWN,\n normalizeInducedProcedure,\n parseInducedProcedure,\n runWorkflowInduction,\n type Trajectory,\n type TrajectoryStep,\n trajectoryFromRunState,\n type VerificationResult,\n type WorkflowInducer,\n type WorkflowInductionOptions,\n} from './phases/induce.js';\nexport {\n type LightPhaseDeps,\n runLightPhase,\n} from './phases/light.js';\nexport {\n parseExtraction,\n runStandardPhase,\n type StandardPhaseDeps,\n} from './phases/standard.js';\nexport { type Consolidator, createConsolidator } from './runtime.js';\nexport {\n type ConsolidatorCatchupPolicy,\n type RegisterTriggersOptions,\n type RegisterTriggersResult,\n registerConsolidatorTriggers,\n type SchedulerLike,\n type TriggerDeclarationLike,\n} from './scheduler.js';\nexport {\n type ParsedTrigger,\n parseTriggerSpec,\n reasonFromTrigger,\n} from './triggers.js';\nexport type {\n ConsolidatorBudgetSnapshot,\n ConsolidatorCeilings,\n ConsolidatorConfig,\n ConsolidatorLastRuns,\n ConsolidatorPhase,\n ConsolidatorStatus,\n ConsolidatorTier,\n ConsolidatorTriggerReason,\n ConsolidatorTriggerSpec,\n CreateConsolidatorOptions,\n OnBudgetExceed,\n PhaseListener,\n PhaseOutcome,\n} from './types.js';\nexport { CONSOLIDATOR_TIER_DEFAULTS } from './types.js';\n\n/**\n * Build a no-op consolidator that honours the full {@link Consolidator}\n * interface but performs no background work. Useful for consumers\n * that want the typed shape (e.g., unit tests of higher tiers) but\n * do not pay the runtime cost of triggers / locking / DLQ.\n *\n * Phase 10c's {@link createConsolidator} is the production factory.\n *\n * @stable\n */\nexport function createConsolidatorPlaceholder(\n options: {\n readonly triggers?: ReadonlyArray<ConsolidatorTriggerSpec>;\n readonly tier?: ConsolidatorTier;\n } = {},\n): Consolidator {\n let running = false;\n let paused = false;\n let tier: ConsolidatorTier = options.tier ?? 'free';\n const triggers = Object.freeze([\n // Matches the real runtime default (MCON-4): idle drives light/standard,\n // the daily cron makes the deep phase reachable; turn:N is consumer-emitted\n // so it is not a scheduler default.\n ...(options.triggers ?? (['idle:5m', 'cron:0 4 * * *'] as ConsolidatorTriggerSpec[])),\n ]) as ReadonlyArray<ConsolidatorTriggerSpec>;\n const listeners = new Set<PhaseListener>();\n\n const config: ConsolidatorConfig = Object.freeze({\n triggers,\n tier,\n phases: Object.freeze(['light' as ConsolidatorPhase]),\n ceilings: {\n maxTokensPerDay: 0,\n maxCostPerDay: 0,\n maxConcurrentRuns: 1,\n maxRunDurationMs: 5 * 60 * 1000,\n cooldownMs: 60_000,\n },\n onExceed: 'pause',\n cheapModel: null,\n deepModel: null,\n budgetResetSemantics: 'utc',\n noiseFilters: Object.freeze(['default' as const]),\n lockWaitMs: 30_000,\n decayTauDays: 7,\n decayArchiveThreshold: 0.05,\n decayCapacity: null,\n salienceWeights: DEFAULT_SALIENCE_WEIGHTS,\n maxStandardBatchSize: 50,\n maxDeepConflictsPerRun: 20,\n dlqMaxRetries: 5,\n dlqBaseBackoffMs: 60_000,\n dlqMaxBackoffMs: 60 * 60 * 1000,\n formEpisodes: false,\n importanceScoring: false,\n autoPromoteExtraction: false,\n reflection: false,\n importanceThreshold: 3,\n reflectionMaxQuestions: 3,\n contextualRetrieval: 'late-chunk',\n learnedContext: false,\n learnedContextMaxChars: 1200,\n });\n\n const status = async (): Promise<ConsolidatorStatus> =>\n Object.freeze({\n tier,\n triggers,\n phases: config.phases,\n running,\n paused,\n pendingConflicts: 0,\n queueDepth: 0,\n dlqSize: 0,\n deferredRuns: 0,\n emptyExtractions: 0,\n lastRuns: Object.freeze({}),\n budget: {\n tokensUsedToday: 0,\n costUsedToday: 0,\n tokensRemaining: 0,\n costRemaining: 0,\n resetAt: new Date(Date.now() + 86_400_000).toISOString(),\n },\n budgetRemaining: {\n tokens: 0,\n costUsd: 0,\n },\n });\n\n return {\n async start() {\n running = true;\n },\n async stop() {\n running = false;\n },\n async trigger() {\n return null;\n },\n async fireNow(): Promise<PhaseOutcome | null> {\n return null;\n },\n async setTier(next: ConsolidatorTier) {\n tier = next;\n },\n async pause() {\n paused = true;\n },\n async resume() {\n paused = false;\n },\n recordExternalSpend(): void {\n // Placeholder - no budget to record against.\n },\n onPhaseFinished(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n config() {\n return config;\n },\n async registerWithScheduler() {\n // No-op consolidator: nothing to schedule.\n return Object.freeze({ registered: Object.freeze([]), skipped: Object.freeze([]) });\n },\n isFree() {\n return tier === 'free';\n },\n async drainDlq() {\n return 0;\n },\n status,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8IA,SAAgB,8BACd,UAGI,EAAE,EACQ;CACd,IAAI,UAAU;CACd,IAAI,SAAS;CACb,IAAIA,OAAyB,QAAQ,QAAQ;CAC7C,MAAM,WAAW,OAAO,OAAO,CAI7B,GAAI,QAAQ,YAAa,CAAC,WAAW,iBAAiB,CACvD,CAAC;CACF,MAAM,4BAAY,IAAI,KAAoB;CAE1C,MAAMC,SAA6B,OAAO,OAAO;EAC/C;EACA;EACA,QAAQ,OAAO,OAAO,CAAC,QAA6B,CAAC;EACrD,UAAU;GACR,iBAAiB;GACjB,eAAe;GACf,mBAAmB;GACnB,kBAAkB,MAAS;GAC3B,YAAY;GACb;EACD,UAAU;EACV,YAAY;EACZ,WAAW;EACX,sBAAsB;EACtB,cAAc,OAAO,OAAO,CAAC,UAAmB,CAAC;EACjD,YAAY;EACZ,cAAc;EACd,uBAAuB;EACvB,eAAe;EACf,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,eAAe;EACf,kBAAkB;EAClB,iBAAiB,OAAU;EAC3B,cAAc;EACd,mBAAmB;EACnB,uBAAuB;EACvB,YAAY;EACZ,qBAAqB;EACrB,wBAAwB;EACxB,qBAAqB;EACrB,gBAAgB;EAChB,wBAAwB;EACzB,CAAC;CAEF,MAAM,SAAS,YACb,OAAO,OAAO;EACZ;EACA;EACA,QAAQ,OAAO;EACf;EACA;EACA,kBAAkB;EAClB,YAAY;EACZ,SAAS;EACT,cAAc;EACd,kBAAkB;EAClB,UAAU,OAAO,OAAO,EAAE,CAAC;EAC3B,QAAQ;GACN,iBAAiB;GACjB,eAAe;GACf,iBAAiB;GACjB,eAAe;GACf,SAAS,IAAI,KAAK,KAAK,KAAK,GAAG,MAAW,CAAC,aAAa;GACzD;EACD,iBAAiB;GACf,QAAQ;GACR,SAAS;GACV;EACF,CAAC;AAEJ,QAAO;EACL,MAAM,QAAQ;AACZ,aAAU;;EAEZ,MAAM,OAAO;AACX,aAAU;;EAEZ,MAAM,UAAU;AACd,UAAO;;EAET,MAAM,UAAwC;AAC5C,UAAO;;EAET,MAAM,QAAQ,MAAwB;AACpC,UAAO;;EAET,MAAM,QAAQ;AACZ,YAAS;;EAEX,MAAM,SAAS;AACb,YAAS;;EAEX,sBAA4B;EAG5B,gBAAgB,UAAU;AACxB,aAAU,IAAI,SAAS;AACvB,gBAAa,UAAU,OAAO,SAAS;;EAEzC,SAAS;AACP,UAAO;;EAET,MAAM,wBAAwB;AAE5B,UAAO,OAAO,OAAO;IAAE,YAAY,OAAO,OAAO,EAAE,CAAC;IAAE,SAAS,OAAO,OAAO,EAAE,CAAC;IAAE,CAAC;;EAErF,SAAS;AACP,UAAO,SAAS;;EAElB,MAAM,WAAW;AACf,UAAO;;EAET;EACD"}
@@ -42,7 +42,7 @@ var LockManager = class {
42
42
  }
43
43
  }
44
44
  /**
45
- * Release the lock. Idempotent releasing an already-released
45
+ * Release the lock. Idempotent - releasing an already-released
46
46
  * lock is a no-op.
47
47
  *
48
48
  * @stable
@@ -1 +1 @@
1
- {"version":3,"file":"lock.js","names":["#store","#waitMs","#maxAge","#now","#sleep","#randomId","#localLocks","#tryAcquire","#heldBy"],"sources":["../../src/consolidator/lock.ts"],"sourcesContent":["/**\n * Wait-then-defer lock manager. The lock state lives in the storage\n * layer's `consolidator_state` row so concurrent processes (lib +\n * server) coordinate through SQLite. The manager waits up to\n * `lockWaitMs` for the lock to clear; if the wait expires it returns\n * `'deferred'` so the trigger is recorded but no phase runs.\n *\n * The lock honours DEC-114 (\"memory does not block the agent loop\")\n * by capping the wait the agent loop never observes a memory\n * blocker.\n *\n * @packageDocumentation\n */\n\nimport type { SessionScope } from '@graphorin/core';\nimport type { ConsolidatorMemoryStoreExt } from '../internal/storage-adapter.js';\n\n/** Outcome surfaced by {@link LockManager.acquire}. */\nexport type LockOutcome =\n | { readonly kind: 'acquired'; readonly runId: string }\n | { readonly kind: 'deferred'; readonly heldBy: string | null };\n\n/** Options accepted by {@link LockManager}. */\nexport interface LockManagerOptions {\n readonly store: ConsolidatorMemoryStoreExt | null;\n readonly waitMs: number;\n readonly maxRunDurationMs: number;\n readonly now?: () => number;\n readonly sleep?: (ms: number) => Promise<void>;\n readonly randomId?: () => string;\n}\n\nconst DEFAULT_POLL_MS = 100;\n\n/** @stable */\nexport class LockManager {\n readonly #store: ConsolidatorMemoryStoreExt | null;\n readonly #waitMs: number;\n readonly #maxAge: number;\n readonly #now: () => number;\n readonly #sleep: (ms: number) => Promise<void>;\n readonly #randomId: () => string;\n // Per-process fallback when the storage adapter does not expose a\n // consolidator surface (lib mode + in-memory test doubles).\n readonly #localLocks = new Map<string, { runId: string; acquiredAt: number }>();\n\n constructor(opts: LockManagerOptions) {\n this.#store = opts.store;\n this.#waitMs = Math.max(0, opts.waitMs);\n this.#maxAge = Math.max(0, opts.maxRunDurationMs);\n this.#now = opts.now ?? Date.now;\n this.#sleep =\n opts.sleep ??\n ((ms): Promise<void> => new Promise((resolve) => globalThis.setTimeout(resolve, ms)));\n this.#randomId =\n opts.randomId ??\n ((): string => {\n const a = Math.floor(Math.random() * 1e9).toString(36);\n const b = Math.floor(Math.random() * 1e9).toString(36);\n return `run_${a}${b}`;\n });\n }\n\n /**\n * Try to acquire the per-scope lock. Returns the run id on success\n * or `'deferred'` after the wait window elapses.\n *\n * @stable\n */\n async acquire(scope: SessionScope): Promise<LockOutcome> {\n const runId = this.#randomId();\n const start = this.#now();\n const pollMs = Math.min(DEFAULT_POLL_MS, Math.max(10, this.#waitMs / 5 || DEFAULT_POLL_MS));\n while (true) {\n const ok = await this.#tryAcquire(scope, runId);\n if (ok) return { kind: 'acquired', runId };\n const elapsed = this.#now() - start;\n if (elapsed >= this.#waitMs) {\n const heldBy = await this.#heldBy(scope);\n return { kind: 'deferred', heldBy };\n }\n await this.#sleep(pollMs);\n }\n }\n\n /**\n * Release the lock. Idempotent releasing an already-released\n * lock is a no-op.\n *\n * @stable\n */\n async release(scope: SessionScope, runId: string): Promise<void> {\n if (this.#store !== null) {\n await this.#store.releaseLock(scope, runId);\n }\n const key = scopeKey(scope);\n const local = this.#localLocks.get(key);\n if (local !== undefined && local.runId === runId) {\n this.#localLocks.delete(key);\n }\n }\n\n async #tryAcquire(scope: SessionScope, runId: string): Promise<boolean> {\n const now = this.#now();\n if (this.#store !== null) {\n return this.#store.acquireLock(scope, runId, now, this.#maxAge);\n }\n // Fall back to per-process map.\n const key = scopeKey(scope);\n const existing = this.#localLocks.get(key);\n if (existing === undefined) {\n this.#localLocks.set(key, { runId, acquiredAt: now });\n return true;\n }\n if (existing.runId === runId) return true;\n if (this.#maxAge > 0 && now - existing.acquiredAt > this.#maxAge) {\n this.#localLocks.set(key, { runId, acquiredAt: now });\n return true;\n }\n return false;\n }\n\n async #heldBy(scope: SessionScope): Promise<string | null> {\n if (this.#store !== null) {\n const state = await this.#store.getState(scope);\n return state?.activeLockHeldBy ?? null;\n }\n const local = this.#localLocks.get(scopeKey(scope));\n return local?.runId ?? null;\n }\n}\n\nfunction scopeKey(scope: SessionScope): string {\n return `${scope.userId}|${scope.sessionId ?? ''}|${scope.agentId ?? ''}`;\n}\n"],"mappings":";AAgCA,MAAM,kBAAkB;;AAGxB,IAAa,cAAb,MAAyB;CACvB,CAASA;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CAGT,CAASC,6BAAc,IAAI,KAAoD;CAE/E,YAAY,MAA0B;AACpC,QAAKN,QAAS,KAAK;AACnB,QAAKC,SAAU,KAAK,IAAI,GAAG,KAAK,OAAO;AACvC,QAAKC,SAAU,KAAK,IAAI,GAAG,KAAK,iBAAiB;AACjD,QAAKC,MAAO,KAAK,OAAO,KAAK;AAC7B,QAAKC,QACH,KAAK,WACH,OAAsB,IAAI,SAAS,YAAY,WAAW,WAAW,SAAS,GAAG,CAAC;AACtF,QAAKC,WACH,KAAK,mBACU;AAGb,UAAO,OAFG,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG,GAC5C,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG;;;;;;;;;CAW5D,MAAM,QAAQ,OAA2C;EACvD,MAAM,QAAQ,MAAKA,UAAW;EAC9B,MAAM,QAAQ,MAAKF,KAAM;EACzB,MAAM,SAAS,KAAK,IAAI,iBAAiB,KAAK,IAAI,IAAI,MAAKF,SAAU,KAAK,gBAAgB,CAAC;AAC3F,SAAO,MAAM;AAEX,OADW,MAAM,MAAKM,WAAY,OAAO,MAAM,CACvC,QAAO;IAAE,MAAM;IAAY;IAAO;AAE1C,OADgB,MAAKJ,KAAM,GAAG,SACf,MAAKF,OAElB,QAAO;IAAE,MAAM;IAAY,QADZ,MAAM,MAAKO,OAAQ,MAAM;IACL;AAErC,SAAM,MAAKJ,MAAO,OAAO;;;;;;;;;CAU7B,MAAM,QAAQ,OAAqB,OAA8B;AAC/D,MAAI,MAAKJ,UAAW,KAClB,OAAM,MAAKA,MAAO,YAAY,OAAO,MAAM;EAE7C,MAAM,MAAM,SAAS,MAAM;EAC3B,MAAM,QAAQ,MAAKM,WAAY,IAAI,IAAI;AACvC,MAAI,UAAU,UAAa,MAAM,UAAU,MACzC,OAAKA,WAAY,OAAO,IAAI;;CAIhC,OAAMC,WAAY,OAAqB,OAAiC;EACtE,MAAM,MAAM,MAAKJ,KAAM;AACvB,MAAI,MAAKH,UAAW,KAClB,QAAO,MAAKA,MAAO,YAAY,OAAO,OAAO,KAAK,MAAKE,OAAQ;EAGjE,MAAM,MAAM,SAAS,MAAM;EAC3B,MAAM,WAAW,MAAKI,WAAY,IAAI,IAAI;AAC1C,MAAI,aAAa,QAAW;AAC1B,SAAKA,WAAY,IAAI,KAAK;IAAE;IAAO,YAAY;IAAK,CAAC;AACrD,UAAO;;AAET,MAAI,SAAS,UAAU,MAAO,QAAO;AACrC,MAAI,MAAKJ,SAAU,KAAK,MAAM,SAAS,aAAa,MAAKA,QAAS;AAChE,SAAKI,WAAY,IAAI,KAAK;IAAE;IAAO,YAAY;IAAK,CAAC;AACrD,UAAO;;AAET,SAAO;;CAGT,OAAME,OAAQ,OAA6C;AACzD,MAAI,MAAKR,UAAW,KAElB,SADc,MAAM,MAAKA,MAAO,SAAS,MAAM,GACjC,oBAAoB;AAGpC,SADc,MAAKM,WAAY,IAAI,SAAS,MAAM,CAAC,EACrC,SAAS;;;AAI3B,SAAS,SAAS,OAA6B;AAC7C,QAAO,GAAG,MAAM,OAAO,GAAG,MAAM,aAAa,GAAG,GAAG,MAAM,WAAW"}
1
+ {"version":3,"file":"lock.js","names":["#store","#waitMs","#maxAge","#now","#sleep","#randomId","#localLocks","#tryAcquire","#heldBy"],"sources":["../../src/consolidator/lock.ts"],"sourcesContent":["/**\n * Wait-then-defer lock manager. The lock state lives in the storage\n * layer's `consolidator_state` row so concurrent processes (lib +\n * server) coordinate through SQLite. The manager waits up to\n * `lockWaitMs` for the lock to clear; if the wait expires it returns\n * `'deferred'` so the trigger is recorded but no phase runs.\n *\n * The lock honours DEC-114 (\"memory does not block the agent loop\")\n * by capping the wait - the agent loop never observes a memory\n * blocker.\n *\n * @packageDocumentation\n */\n\nimport type { SessionScope } from '@graphorin/core';\nimport type { ConsolidatorMemoryStoreExt } from '../internal/storage-adapter.js';\n\n/** Outcome surfaced by {@link LockManager.acquire}. */\nexport type LockOutcome =\n | { readonly kind: 'acquired'; readonly runId: string }\n | { readonly kind: 'deferred'; readonly heldBy: string | null };\n\n/** Options accepted by {@link LockManager}. */\nexport interface LockManagerOptions {\n readonly store: ConsolidatorMemoryStoreExt | null;\n readonly waitMs: number;\n readonly maxRunDurationMs: number;\n readonly now?: () => number;\n readonly sleep?: (ms: number) => Promise<void>;\n readonly randomId?: () => string;\n}\n\nconst DEFAULT_POLL_MS = 100;\n\n/** @stable */\nexport class LockManager {\n readonly #store: ConsolidatorMemoryStoreExt | null;\n readonly #waitMs: number;\n readonly #maxAge: number;\n readonly #now: () => number;\n readonly #sleep: (ms: number) => Promise<void>;\n readonly #randomId: () => string;\n // Per-process fallback when the storage adapter does not expose a\n // consolidator surface (lib mode + in-memory test doubles).\n readonly #localLocks = new Map<string, { runId: string; acquiredAt: number }>();\n\n constructor(opts: LockManagerOptions) {\n this.#store = opts.store;\n this.#waitMs = Math.max(0, opts.waitMs);\n this.#maxAge = Math.max(0, opts.maxRunDurationMs);\n this.#now = opts.now ?? Date.now;\n this.#sleep =\n opts.sleep ??\n ((ms): Promise<void> => new Promise((resolve) => globalThis.setTimeout(resolve, ms)));\n this.#randomId =\n opts.randomId ??\n ((): string => {\n const a = Math.floor(Math.random() * 1e9).toString(36);\n const b = Math.floor(Math.random() * 1e9).toString(36);\n return `run_${a}${b}`;\n });\n }\n\n /**\n * Try to acquire the per-scope lock. Returns the run id on success\n * or `'deferred'` after the wait window elapses.\n *\n * @stable\n */\n async acquire(scope: SessionScope): Promise<LockOutcome> {\n const runId = this.#randomId();\n const start = this.#now();\n const pollMs = Math.min(DEFAULT_POLL_MS, Math.max(10, this.#waitMs / 5 || DEFAULT_POLL_MS));\n while (true) {\n const ok = await this.#tryAcquire(scope, runId);\n if (ok) return { kind: 'acquired', runId };\n const elapsed = this.#now() - start;\n if (elapsed >= this.#waitMs) {\n const heldBy = await this.#heldBy(scope);\n return { kind: 'deferred', heldBy };\n }\n await this.#sleep(pollMs);\n }\n }\n\n /**\n * Release the lock. Idempotent - releasing an already-released\n * lock is a no-op.\n *\n * @stable\n */\n async release(scope: SessionScope, runId: string): Promise<void> {\n if (this.#store !== null) {\n await this.#store.releaseLock(scope, runId);\n }\n const key = scopeKey(scope);\n const local = this.#localLocks.get(key);\n if (local !== undefined && local.runId === runId) {\n this.#localLocks.delete(key);\n }\n }\n\n async #tryAcquire(scope: SessionScope, runId: string): Promise<boolean> {\n const now = this.#now();\n if (this.#store !== null) {\n return this.#store.acquireLock(scope, runId, now, this.#maxAge);\n }\n // Fall back to per-process map.\n const key = scopeKey(scope);\n const existing = this.#localLocks.get(key);\n if (existing === undefined) {\n this.#localLocks.set(key, { runId, acquiredAt: now });\n return true;\n }\n if (existing.runId === runId) return true;\n if (this.#maxAge > 0 && now - existing.acquiredAt > this.#maxAge) {\n this.#localLocks.set(key, { runId, acquiredAt: now });\n return true;\n }\n return false;\n }\n\n async #heldBy(scope: SessionScope): Promise<string | null> {\n if (this.#store !== null) {\n const state = await this.#store.getState(scope);\n return state?.activeLockHeldBy ?? null;\n }\n const local = this.#localLocks.get(scopeKey(scope));\n return local?.runId ?? null;\n }\n}\n\nfunction scopeKey(scope: SessionScope): string {\n return `${scope.userId}|${scope.sessionId ?? ''}|${scope.agentId ?? ''}`;\n}\n"],"mappings":";AAgCA,MAAM,kBAAkB;;AAGxB,IAAa,cAAb,MAAyB;CACvB,CAASA;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CAGT,CAASC,6BAAc,IAAI,KAAoD;CAE/E,YAAY,MAA0B;AACpC,QAAKN,QAAS,KAAK;AACnB,QAAKC,SAAU,KAAK,IAAI,GAAG,KAAK,OAAO;AACvC,QAAKC,SAAU,KAAK,IAAI,GAAG,KAAK,iBAAiB;AACjD,QAAKC,MAAO,KAAK,OAAO,KAAK;AAC7B,QAAKC,QACH,KAAK,WACH,OAAsB,IAAI,SAAS,YAAY,WAAW,WAAW,SAAS,GAAG,CAAC;AACtF,QAAKC,WACH,KAAK,mBACU;AAGb,UAAO,OAFG,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG,GAC5C,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI,CAAC,SAAS,GAAG;;;;;;;;;CAW5D,MAAM,QAAQ,OAA2C;EACvD,MAAM,QAAQ,MAAKA,UAAW;EAC9B,MAAM,QAAQ,MAAKF,KAAM;EACzB,MAAM,SAAS,KAAK,IAAI,iBAAiB,KAAK,IAAI,IAAI,MAAKF,SAAU,KAAK,gBAAgB,CAAC;AAC3F,SAAO,MAAM;AAEX,OADW,MAAM,MAAKM,WAAY,OAAO,MAAM,CACvC,QAAO;IAAE,MAAM;IAAY;IAAO;AAE1C,OADgB,MAAKJ,KAAM,GAAG,SACf,MAAKF,OAElB,QAAO;IAAE,MAAM;IAAY,QADZ,MAAM,MAAKO,OAAQ,MAAM;IACL;AAErC,SAAM,MAAKJ,MAAO,OAAO;;;;;;;;;CAU7B,MAAM,QAAQ,OAAqB,OAA8B;AAC/D,MAAI,MAAKJ,UAAW,KAClB,OAAM,MAAKA,MAAO,YAAY,OAAO,MAAM;EAE7C,MAAM,MAAM,SAAS,MAAM;EAC3B,MAAM,QAAQ,MAAKM,WAAY,IAAI,IAAI;AACvC,MAAI,UAAU,UAAa,MAAM,UAAU,MACzC,OAAKA,WAAY,OAAO,IAAI;;CAIhC,OAAMC,WAAY,OAAqB,OAAiC;EACtE,MAAM,MAAM,MAAKJ,KAAM;AACvB,MAAI,MAAKH,UAAW,KAClB,QAAO,MAAKA,MAAO,YAAY,OAAO,OAAO,KAAK,MAAKE,OAAQ;EAGjE,MAAM,MAAM,SAAS,MAAM;EAC3B,MAAM,WAAW,MAAKI,WAAY,IAAI,IAAI;AAC1C,MAAI,aAAa,QAAW;AAC1B,SAAKA,WAAY,IAAI,KAAK;IAAE;IAAO,YAAY;IAAK,CAAC;AACrD,UAAO;;AAET,MAAI,SAAS,UAAU,MAAO,QAAO;AACrC,MAAI,MAAKJ,SAAU,KAAK,MAAM,SAAS,aAAa,MAAKA,QAAS;AAChE,SAAKI,WAAY,IAAI,KAAK;IAAE;IAAO,YAAY;IAAK,CAAC;AACrD,UAAO;;AAET,SAAO;;CAGT,OAAME,OAAQ,OAA6C;AACzD,MAAI,MAAKR,UAAW,KAElB,SADc,MAAM,MAAKA,MAAO,SAAS,MAAM,GACjC,oBAAoB;AAGpC,SADc,MAAKM,WAAY,IAAI,SAAS,MAAM,CAAC,EACrC,SAAS;;;AAI3B,SAAS,SAAS,OAA6B;AAC7C,QAAO,GAAG,MAAM,OAAO,GAAG,MAAM,aAAa,GAAG,GAAG,MAAM,WAAW"}
@@ -1 +1 @@
1
- {"version":3,"file":"noise-filter.js","names":["droppedByReason: Record<string, number>","kept: SessionMessageRecord[]","recentTexts: string[]"],"sources":["../../src/consolidator/noise-filter.ts"],"sourcesContent":["/**\n * Built-in noise filter pipeline. Runs before LLM extraction in the\n * standard phase + every light-phase invocation. The defaults are\n * heuristic-only (no LLM); adapters may extend the pipeline through\n * `noiseFilters: [...customFilters]` once the public surface is wired\n * (post-MVP DEC-134 carve-out).\n *\n * @packageDocumentation\n */\n\nimport type { Message } from '@graphorin/core';\nimport type { SessionMessageRecord } from '../internal/storage-adapter.js';\n\n/**\n * Bundle level surfaced by {@link applyNoiseFilters}. The runtime\n * uses these to log + report `noise_filtered_count` per phase.\n *\n * @stable\n */\nexport type NoiseFilterPreset = 'default' | 'minimal' | 'none';\n\nconst MIN_LENGTH_DEFAULT = 10;\nconst MIN_LENGTH_MINIMAL = 4;\nconst MAX_LENGTH = 10_000;\nconst STOP_WORD_RATIO = 0.8;\nconst STOP_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'are',\n 'as',\n 'at',\n 'be',\n 'but',\n 'by',\n 'can',\n 'do',\n 'does',\n 'for',\n 'from',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'his',\n 'i',\n 'if',\n 'in',\n 'is',\n 'it',\n 'its',\n 'me',\n 'my',\n 'no',\n 'not',\n 'of',\n 'on',\n 'or',\n 'our',\n 'she',\n 'so',\n 'that',\n 'the',\n 'their',\n 'them',\n 'they',\n 'this',\n 'to',\n 'too',\n 'us',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'who',\n 'will',\n 'with',\n 'would',\n 'yes',\n 'you',\n 'your',\n]);\n\n/**\n * Result returned by {@link applyNoiseFilters}.\n *\n * @stable\n */\nexport interface NoiseFilterResult {\n readonly kept: ReadonlyArray<SessionMessageRecord>;\n readonly droppedCount: number;\n readonly droppedByReason: Readonly<Record<string, number>>;\n}\n\n/**\n * Run the configured filter chain against the supplied messages.\n * Filters short-circuit at the first hit so the cheapest checks\n * always run first.\n *\n * @stable\n */\nexport function applyNoiseFilters(\n messages: ReadonlyArray<SessionMessageRecord>,\n presets: ReadonlyArray<NoiseFilterPreset> = ['default'],\n): NoiseFilterResult {\n if (presets.length === 0 || presets.every((p) => p === 'none')) {\n return { kept: messages, droppedCount: 0, droppedByReason: Object.freeze({}) };\n }\n const minimal = presets.includes('minimal') && !presets.includes('default');\n const minLength = minimal ? MIN_LENGTH_MINIMAL : MIN_LENGTH_DEFAULT;\n const droppedByReason: Record<string, number> = {};\n const kept: SessionMessageRecord[] = [];\n const recentTexts: string[] = [];\n\n for (const record of messages) {\n const reason = classify(record.message, recentTexts, minLength, minimal);\n if (reason === null) {\n kept.push(record);\n const text = renderText(record.message);\n if (text.length > 0) {\n recentTexts.push(text);\n if (recentTexts.length > 5) recentTexts.shift();\n }\n } else {\n droppedByReason[reason] = (droppedByReason[reason] ?? 0) + 1;\n }\n }\n return {\n kept,\n droppedCount: messages.length - kept.length,\n droppedByReason: Object.freeze({ ...droppedByReason }),\n };\n}\n\nfunction classify(\n message: Message,\n recentTexts: ReadonlyArray<string>,\n minLength: number,\n minimal: boolean,\n): string | null {\n if (message.role === 'system') return 'role-system';\n if (message.role === 'tool' && minimal) return null;\n const text = renderText(message);\n const trimmed = text.trim();\n if (trimmed.length === 0) return 'empty';\n if (trimmed.length < minLength) return 'too-short';\n if (trimmed.length > MAX_LENGTH) return 'too-long';\n if (looksLikeCodeOrJson(trimmed)) return 'code-block';\n if (!minimal && stopWordRatio(trimmed) > STOP_WORD_RATIO) return 'stop-word-ratio';\n if (!minimal && isNearDuplicate(trimmed, recentTexts)) return 'near-duplicate';\n return null;\n}\n\n/**\n * Render a message as a single text blob for the heuristic filters.\n *\n * @internal\n */\nexport function renderText(message: Message): string {\n if (message.role === 'system') return message.content;\n const c = message.content;\n if (typeof c === 'string') return c;\n let out = '';\n for (const part of c) {\n if (part.type === 'text' || part.type === 'reasoning') {\n out += `${part.text} `;\n }\n }\n return out.trim();\n}\n\nfunction looksLikeCodeOrJson(text: string): boolean {\n if (text.startsWith('```')) return true;\n const trimmed = text.trim();\n if (\n (trimmed.startsWith('{') && trimmed.endsWith('}')) ||\n (trimmed.startsWith('[') && trimmed.endsWith(']'))\n ) {\n try {\n JSON.parse(trimmed);\n return true;\n } catch {\n // Not actually JSON fall through.\n }\n }\n return false;\n}\n\nfunction stopWordRatio(text: string): number {\n const tokens = text\n .toLowerCase()\n .split(/[^a-z0-9]+/u)\n .filter((t) => t.length > 0);\n if (tokens.length === 0) return 0;\n let stop = 0;\n for (const t of tokens) if (STOP_WORDS.has(t)) stop += 1;\n return stop / tokens.length;\n}\n\nfunction isNearDuplicate(text: string, recent: ReadonlyArray<string>): boolean {\n const normalised = text.replace(/\\s+/gu, ' ').toLowerCase().trim();\n for (const prior of recent) {\n const priorNorm = prior.replace(/\\s+/gu, ' ').toLowerCase().trim();\n if (priorNorm.length === 0) continue;\n if (priorNorm === normalised) return true;\n const similarity = trigramSimilarity(priorNorm, normalised);\n if (similarity >= 0.9) return true;\n }\n return false;\n}\n\nfunction trigramSimilarity(a: string, b: string): number {\n const setA = trigramSet(a);\n const setB = trigramSet(b);\n if (setA.size === 0 || setB.size === 0) return 0;\n let inter = 0;\n for (const t of setA) if (setB.has(t)) inter += 1;\n return inter / Math.max(setA.size, setB.size);\n}\n\nfunction trigramSet(text: string): Set<string> {\n const out = new Set<string>();\n if (text.length < 3) {\n out.add(text);\n return out;\n }\n for (let i = 0; i < text.length - 2; i++) {\n out.add(text.slice(i, i + 3));\n }\n return out;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"noise-filter.js","names":["droppedByReason: Record<string, number>","kept: SessionMessageRecord[]","recentTexts: string[]"],"sources":["../../src/consolidator/noise-filter.ts"],"sourcesContent":["/**\n * Built-in noise filter pipeline. Runs before LLM extraction in the\n * standard phase + every light-phase invocation. The defaults are\n * heuristic-only (no LLM); adapters may extend the pipeline through\n * `noiseFilters: [...customFilters]` once the public surface is wired\n * (post-MVP - DEC-134 carve-out).\n *\n * @packageDocumentation\n */\n\nimport type { Message } from '@graphorin/core';\nimport type { SessionMessageRecord } from '../internal/storage-adapter.js';\n\n/**\n * Bundle level surfaced by {@link applyNoiseFilters}. The runtime\n * uses these to log + report `noise_filtered_count` per phase.\n *\n * @stable\n */\nexport type NoiseFilterPreset = 'default' | 'minimal' | 'none';\n\nconst MIN_LENGTH_DEFAULT = 10;\nconst MIN_LENGTH_MINIMAL = 4;\nconst MAX_LENGTH = 10_000;\nconst STOP_WORD_RATIO = 0.8;\nconst STOP_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'are',\n 'as',\n 'at',\n 'be',\n 'but',\n 'by',\n 'can',\n 'do',\n 'does',\n 'for',\n 'from',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'his',\n 'i',\n 'if',\n 'in',\n 'is',\n 'it',\n 'its',\n 'me',\n 'my',\n 'no',\n 'not',\n 'of',\n 'on',\n 'or',\n 'our',\n 'she',\n 'so',\n 'that',\n 'the',\n 'their',\n 'them',\n 'they',\n 'this',\n 'to',\n 'too',\n 'us',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'who',\n 'will',\n 'with',\n 'would',\n 'yes',\n 'you',\n 'your',\n]);\n\n/**\n * Result returned by {@link applyNoiseFilters}.\n *\n * @stable\n */\nexport interface NoiseFilterResult {\n readonly kept: ReadonlyArray<SessionMessageRecord>;\n readonly droppedCount: number;\n readonly droppedByReason: Readonly<Record<string, number>>;\n}\n\n/**\n * Run the configured filter chain against the supplied messages.\n * Filters short-circuit at the first hit so the cheapest checks\n * always run first.\n *\n * @stable\n */\nexport function applyNoiseFilters(\n messages: ReadonlyArray<SessionMessageRecord>,\n presets: ReadonlyArray<NoiseFilterPreset> = ['default'],\n): NoiseFilterResult {\n if (presets.length === 0 || presets.every((p) => p === 'none')) {\n return { kept: messages, droppedCount: 0, droppedByReason: Object.freeze({}) };\n }\n const minimal = presets.includes('minimal') && !presets.includes('default');\n const minLength = minimal ? MIN_LENGTH_MINIMAL : MIN_LENGTH_DEFAULT;\n const droppedByReason: Record<string, number> = {};\n const kept: SessionMessageRecord[] = [];\n const recentTexts: string[] = [];\n\n for (const record of messages) {\n const reason = classify(record.message, recentTexts, minLength, minimal);\n if (reason === null) {\n kept.push(record);\n const text = renderText(record.message);\n if (text.length > 0) {\n recentTexts.push(text);\n if (recentTexts.length > 5) recentTexts.shift();\n }\n } else {\n droppedByReason[reason] = (droppedByReason[reason] ?? 0) + 1;\n }\n }\n return {\n kept,\n droppedCount: messages.length - kept.length,\n droppedByReason: Object.freeze({ ...droppedByReason }),\n };\n}\n\nfunction classify(\n message: Message,\n recentTexts: ReadonlyArray<string>,\n minLength: number,\n minimal: boolean,\n): string | null {\n if (message.role === 'system') return 'role-system';\n if (message.role === 'tool' && minimal) return null;\n const text = renderText(message);\n const trimmed = text.trim();\n if (trimmed.length === 0) return 'empty';\n if (trimmed.length < minLength) return 'too-short';\n if (trimmed.length > MAX_LENGTH) return 'too-long';\n if (looksLikeCodeOrJson(trimmed)) return 'code-block';\n if (!minimal && stopWordRatio(trimmed) > STOP_WORD_RATIO) return 'stop-word-ratio';\n if (!minimal && isNearDuplicate(trimmed, recentTexts)) return 'near-duplicate';\n return null;\n}\n\n/**\n * Render a message as a single text blob for the heuristic filters.\n *\n * @internal\n */\nexport function renderText(message: Message): string {\n if (message.role === 'system') return message.content;\n const c = message.content;\n if (typeof c === 'string') return c;\n let out = '';\n for (const part of c) {\n if (part.type === 'text' || part.type === 'reasoning') {\n out += `${part.text} `;\n }\n }\n return out.trim();\n}\n\nfunction looksLikeCodeOrJson(text: string): boolean {\n if (text.startsWith('```')) return true;\n const trimmed = text.trim();\n if (\n (trimmed.startsWith('{') && trimmed.endsWith('}')) ||\n (trimmed.startsWith('[') && trimmed.endsWith(']'))\n ) {\n try {\n JSON.parse(trimmed);\n return true;\n } catch {\n // Not actually JSON - fall through.\n }\n }\n return false;\n}\n\nfunction stopWordRatio(text: string): number {\n const tokens = text\n .toLowerCase()\n .split(/[^a-z0-9]+/u)\n .filter((t) => t.length > 0);\n if (tokens.length === 0) return 0;\n let stop = 0;\n for (const t of tokens) if (STOP_WORDS.has(t)) stop += 1;\n return stop / tokens.length;\n}\n\nfunction isNearDuplicate(text: string, recent: ReadonlyArray<string>): boolean {\n const normalised = text.replace(/\\s+/gu, ' ').toLowerCase().trim();\n for (const prior of recent) {\n const priorNorm = prior.replace(/\\s+/gu, ' ').toLowerCase().trim();\n if (priorNorm.length === 0) continue;\n if (priorNorm === normalised) return true;\n const similarity = trigramSimilarity(priorNorm, normalised);\n if (similarity >= 0.9) return true;\n }\n return false;\n}\n\nfunction trigramSimilarity(a: string, b: string): number {\n const setA = trigramSet(a);\n const setB = trigramSet(b);\n if (setA.size === 0 || setB.size === 0) return 0;\n let inter = 0;\n for (const t of setA) if (setB.has(t)) inter += 1;\n return inter / Math.max(setA.size, setB.size);\n}\n\nfunction trigramSet(text: string): Set<string> {\n const out = new Set<string>();\n if (text.length < 3) {\n out.add(text);\n return out;\n }\n for (let i = 0; i < text.length - 2; i++) {\n out.add(text.slice(i, i + 3));\n }\n return out;\n}\n"],"mappings":"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"}
@@ -52,6 +52,11 @@ async function runDeepPhase(deps) {
52
52
  const conflictingId = row.conflictingIds[0] ?? null;
53
53
  let existingText = null;
54
54
  if (conflictingId !== null && typeof semantic.get === "function") existingText = (await semantic.get(conflictingId))?.text ?? null;
55
+ if (conflictingId !== null && existingText === null) {
56
+ await conflicts.markResolved(row.id, "admit");
57
+ resolved += 1;
58
+ continue;
59
+ }
55
60
  const request = buildJudgeRequest(deps, candidateText, existingText);
56
61
  let response;
57
62
  try {
@@ -89,8 +94,7 @@ async function runDeepPhase(deps) {
89
94
  }
90
95
  await conflicts.markResolved(row.id, "supersede");
91
96
  } else if (judge.decision === "dedup" && conflictingId !== null) {
92
- if (typeof semantic.purge === "function") await semantic.purge(row.factId, judge.reason);
93
- else if (typeof semantic.forget === "function") await semantic.forget(row.factId, judge.reason);
97
+ if (typeof semantic.forget === "function") await semantic.forget(row.factId, judge.reason);
94
98
  factsUpdated += 1;
95
99
  await conflicts.markResolved(row.id, "dedup");
96
100
  } else await conflicts.markResolved(row.id, "admit");
@@ -1 +1 @@
1
- {"version":3,"file":"deep.js","names":["existingText: string | null","response: Awaited<ReturnType<Provider['generate']>>","parsed: unknown"],"sources":["../../../src/consolidator/phases/deep.ts"],"sourcesContent":["/**\n * Deep phase — minimum-viable drain of `conflict_check_pending`. The\n * MVP scope is exactly the RB-02 cross-cut: read every queued\n * candidate, ask the configured deep-tier provider to choose between\n * `dedup` / `supersede` / `admit`, apply the resolution, and clear\n * the row.\n *\n * Reflection / fact evolution / sleeptime updates are out of scope\n * (deferred per Phase 10c § Out of scope).\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type {\n ConsolidatorMemoryStoreExt,\n MemoryStoreAdapter,\n} from '../../internal/storage-adapter.js';\nimport type { BudgetTracker } from '../budget.js';\nimport type { PhaseOutcome } from '../types.js';\n\n/** Inputs accepted by {@link runDeepPhase}. */\nexport interface DeepPhaseDeps {\n readonly store: MemoryStoreAdapter;\n readonly consolidatorStore: ConsolidatorMemoryStoreExt | null;\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n readonly deepModel: string | null;\n readonly maxConflictsPerRun: number;\n readonly budget: BudgetTracker;\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n readonly tier: 'standard' | 'full' | 'custom';\n /** Override the wall clock — used by tests + the runtime clock seam. */\n readonly now?: () => number;\n}\n\nconst JUDGE_PROMPT = [\n 'You are a conflict-resolution judge for a long-running personal-assistant memory.',\n 'Decide whether a candidate fact contradicts, dedups, or is unrelated to an existing fact.',\n 'Return strictly JSON: { \"decision\": \"supersede\" | \"dedup\" | \"admit\", \"reason\": string }.',\n 'Use \"supersede\" only when the candidate clearly replaces the older fact.',\n 'Use \"dedup\" when both convey the same information.',\n 'Use \"admit\" when the facts are independent or you are unsure.',\n].join(' ');\n\ninterface JudgeOutcome {\n readonly decision: 'supersede' | 'dedup' | 'admit';\n readonly reason: string;\n}\n\n/** Run the deep phase. */\nexport async function runDeepPhase(deps: DeepPhaseDeps): Promise<PhaseOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.deep',\n deps.scope,\n {\n 'consolidator.phase': 'deep',\n 'consolidator.tier': deps.tier,\n 'consolidator.deep_model': deps.deepModel ?? deps.provider.modelId,\n },\n async (span) => {\n const startedAt = (typeof deps.now === 'function' ? deps.now : Date.now)();\n const conflicts = deps.store.conflicts;\n if (conflicts === undefined) {\n span.setAttributes({\n 'consolidator.deep.skipped': 'no-conflict-store',\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return makeOutcome('completed', { conflictsResolved: 0 });\n }\n const semantic = deps.store.semantic;\n const pending = await conflicts.listPending(deps.scope, deps.maxConflictsPerRun);\n span.setAttributes({ 'consolidator.deep.pending_count': pending.length });\n if (pending.length === 0) {\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return makeOutcome('completed', { conflictsResolved: 0 });\n }\n\n let resolved = 0;\n let factsUpdated = 0;\n let totalTokens = 0;\n let totalCost = 0;\n\n // MCON-9: a judge call that throws or returns garbage must not be\n // re-billed forever. First failure stamps `attemptedAt`; a failure\n // on an already-attempted row closes it (`'judge-unparseable'`),\n // so a poisoned row costs at most two calls across all runs.\n const recordJudgeFailure = async (r: (typeof pending)[number]): Promise<void> => {\n if (r.attemptedAt !== null) {\n await conflicts.markResolved(r.id, 'judge-unparseable');\n } else if (typeof conflicts.markAttempted === 'function') {\n await conflicts.markAttempted(r.id);\n }\n };\n\n for (const row of pending) {\n // MCON-9: mirror the standard/reflect phases — once the budget\n // pauses mid-run, stop spending instead of draining up to\n // maxConflictsPerRun calls past the ceiling.\n if (deps.budget.snapshot().paused) break;\n const candidateText = row.candidateText;\n const conflictingId = row.conflictingIds[0] ?? null;\n let existingText: string | null = null;\n if (conflictingId !== null && typeof semantic.get === 'function') {\n const fact = await semantic.get(conflictingId);\n existingText = fact?.text ?? null;\n }\n const request = buildJudgeRequest(deps, candidateText, existingText);\n let response: Awaited<ReturnType<Provider['generate']>>;\n try {\n response = await deps.provider.generate(request);\n } catch (err) {\n span.recordException(err);\n await recordJudgeFailure(row);\n continue;\n }\n const usage = response.usage;\n const tokens =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n const cost =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n }) ?? 0;\n totalTokens += tokens;\n totalCost += cost;\n deps.budget.record({ phase: 'deep', tokens, costUsd: cost });\n\n const judge = parseJudge(response.text);\n if (judge === null) {\n await recordJudgeFailure(row);\n continue;\n }\n\n if (judge.decision === 'supersede' && conflictingId !== null) {\n if (typeof semantic.get === 'function') {\n const candidateFact = await semantic.get(row.factId);\n if (candidateFact !== null) {\n await semantic.supersede(conflictingId, candidateFact, judge.reason);\n factsUpdated += 1;\n }\n }\n await conflicts.markResolved(row.id, 'supersede');\n } else if (judge.decision === 'dedup' && conflictingId !== null) {\n if (typeof semantic.purge === 'function') {\n await semantic.purge(row.factId, judge.reason);\n } else if (typeof semantic.forget === 'function') {\n await semantic.forget(row.factId, judge.reason);\n }\n factsUpdated += 1;\n await conflicts.markResolved(row.id, 'dedup');\n } else {\n await conflicts.markResolved(row.id, 'admit');\n }\n resolved += 1;\n }\n\n const snapshot = deps.budget.snapshot();\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': factsUpdated,\n 'consolidator.budget_used_usd': totalCost,\n 'consolidator.deep.resolved': resolved,\n 'consolidator.deep.facts_updated': factsUpdated,\n 'consolidator.deep.tokens.total': totalTokens,\n 'consolidator.deep.cost.estimate.usd': totalCost,\n 'consolidator.budget.remaining.tokens': snapshot.tokensRemaining,\n 'consolidator.budget.remaining.usd': snapshot.costRemaining,\n });\n\n return {\n phase: 'deep',\n status: 'completed',\n factsCreated: 0,\n factsUpdated,\n conflictsResolved: resolved,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: totalTokens,\n llmCostUsd: totalCost,\n errorMessage: null,\n };\n },\n );\n}\n\nfunction buildJudgeRequest(\n deps: DeepPhaseDeps,\n candidateText: string,\n existingText: string | null,\n): ProviderRequest {\n const userBlock = [\n `Candidate fact: ${candidateText}`,\n existingText !== null ? `Existing fact: ${existingText}` : 'Existing fact: (unknown)',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: JUDGE_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap — the verdict shape is tiny.\n maxTokens: 256,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\nfunction parseJudge(text: string | undefined): JudgeOutcome | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return null;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return null;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return null;\n const obj = parsed as { decision?: unknown; reason?: unknown };\n const decision = obj.decision;\n if (decision !== 'supersede' && decision !== 'dedup' && decision !== 'admit') return null;\n const reason = typeof obj.reason === 'string' ? obj.reason : 'unspecified';\n return { decision, reason };\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n\nfunction makeOutcome(\n status: PhaseOutcome['status'],\n overrides: Partial<PhaseOutcome>,\n): PhaseOutcome {\n return {\n phase: 'deep',\n status,\n factsCreated: 0,\n factsUpdated: 0,\n conflictsResolved: 0,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: 0,\n llmCostUsd: null,\n errorMessage: null,\n ...overrides,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"deep.js","names":["existingText: string | null","response: Awaited<ReturnType<Provider['generate']>>","parsed: unknown"],"sources":["../../../src/consolidator/phases/deep.ts"],"sourcesContent":["/**\n * Deep phase - minimum-viable drain of `conflict_check_pending`. The\n * MVP scope is exactly the RB-02 cross-cut: read every queued\n * candidate, ask the configured deep-tier provider to choose between\n * `dedup` / `supersede` / `admit`, apply the resolution, and clear\n * the row.\n *\n * Reflection / fact evolution / sleeptime updates are out of scope\n * (deferred per Phase 10c § Out of scope).\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderRequest, SessionScope, Tracer } from '@graphorin/core';\nimport { withMemorySpan } from '../../internal/spans.js';\nimport type {\n ConsolidatorMemoryStoreExt,\n MemoryStoreAdapter,\n} from '../../internal/storage-adapter.js';\nimport type { BudgetTracker } from '../budget.js';\nimport type { PhaseOutcome } from '../types.js';\n\n/** Inputs accepted by {@link runDeepPhase}. */\nexport interface DeepPhaseDeps {\n readonly store: MemoryStoreAdapter;\n readonly consolidatorStore: ConsolidatorMemoryStoreExt | null;\n readonly provider: Provider;\n readonly tracer: Tracer;\n readonly scope: SessionScope;\n readonly deepModel: string | null;\n readonly maxConflictsPerRun: number;\n readonly budget: BudgetTracker;\n readonly priceUsage?: (usage: { promptTokens: number; completionTokens: number }) => number;\n readonly tier: 'standard' | 'full' | 'custom';\n /** Override the wall clock - used by tests + the runtime clock seam. */\n readonly now?: () => number;\n}\n\nconst JUDGE_PROMPT = [\n 'You are a conflict-resolution judge for a long-running personal-assistant memory.',\n 'Decide whether a candidate fact contradicts, dedups, or is unrelated to an existing fact.',\n 'Return strictly JSON: { \"decision\": \"supersede\" | \"dedup\" | \"admit\", \"reason\": string }.',\n 'Use \"supersede\" only when the candidate clearly replaces the older fact.',\n 'Use \"dedup\" when both convey the same information.',\n 'Use \"admit\" when the facts are independent or you are unsure.',\n].join(' ');\n\ninterface JudgeOutcome {\n readonly decision: 'supersede' | 'dedup' | 'admit';\n readonly reason: string;\n}\n\n/** Run the deep phase. */\nexport async function runDeepPhase(deps: DeepPhaseDeps): Promise<PhaseOutcome> {\n return withMemorySpan(\n deps.tracer,\n 'memory.consolidate.deep',\n deps.scope,\n {\n 'consolidator.phase': 'deep',\n 'consolidator.tier': deps.tier,\n 'consolidator.deep_model': deps.deepModel ?? deps.provider.modelId,\n },\n async (span) => {\n const startedAt = (typeof deps.now === 'function' ? deps.now : Date.now)();\n const conflicts = deps.store.conflicts;\n if (conflicts === undefined) {\n span.setAttributes({\n 'consolidator.deep.skipped': 'no-conflict-store',\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return makeOutcome('completed', { conflictsResolved: 0 });\n }\n const semantic = deps.store.semantic;\n const pending = await conflicts.listPending(deps.scope, deps.maxConflictsPerRun);\n span.setAttributes({ 'consolidator.deep.pending_count': pending.length });\n if (pending.length === 0) {\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': 0,\n 'consolidator.budget_used_usd': 0,\n });\n return makeOutcome('completed', { conflictsResolved: 0 });\n }\n\n let resolved = 0;\n let factsUpdated = 0;\n let totalTokens = 0;\n let totalCost = 0;\n\n // MCON-9: a judge call that throws or returns garbage must not be\n // re-billed forever. First failure stamps `attemptedAt`; a failure\n // on an already-attempted row closes it (`'judge-unparseable'`),\n // so a poisoned row costs at most two calls across all runs.\n const recordJudgeFailure = async (r: (typeof pending)[number]): Promise<void> => {\n if (r.attemptedAt !== null) {\n await conflicts.markResolved(r.id, 'judge-unparseable');\n } else if (typeof conflicts.markAttempted === 'function') {\n await conflicts.markAttempted(r.id);\n }\n };\n\n for (const row of pending) {\n // MCON-9: mirror the standard/reflect phases - once the budget\n // pauses mid-run, stop spending instead of draining up to\n // maxConflictsPerRun calls past the ceiling.\n if (deps.budget.snapshot().paused) break;\n const candidateText = row.candidateText;\n const conflictingId = row.conflictingIds[0] ?? null;\n let existingText: string | null = null;\n if (conflictingId !== null && typeof semantic.get === 'function') {\n const fact = await semantic.get(conflictingId);\n existingText = fact?.text ?? null;\n }\n // memory-consolidation-01: the conflicting fact vanished between\n // enqueue and drain (forgotten / purged / superseded) - there is\n // nothing to judge against. Admit the candidate WITHOUT a\n // provider call: a 'dedup' verdict against \"(unknown)\" would\n // delete the only surviving copy.\n if (conflictingId !== null && existingText === null) {\n await conflicts.markResolved(row.id, 'admit');\n resolved += 1;\n continue;\n }\n const request = buildJudgeRequest(deps, candidateText, existingText);\n let response: Awaited<ReturnType<Provider['generate']>>;\n try {\n response = await deps.provider.generate(request);\n } catch (err) {\n span.recordException(err);\n await recordJudgeFailure(row);\n continue;\n }\n const usage = response.usage;\n const tokens =\n (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0) + (usage.reasoningTokens ?? 0);\n const cost =\n deps.priceUsage?.({\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n }) ?? 0;\n totalTokens += tokens;\n totalCost += cost;\n deps.budget.record({ phase: 'deep', tokens, costUsd: cost });\n\n const judge = parseJudge(response.text);\n if (judge === null) {\n await recordJudgeFailure(row);\n continue;\n }\n\n if (judge.decision === 'supersede' && conflictingId !== null) {\n if (typeof semantic.get === 'function') {\n const candidateFact = await semantic.get(row.factId);\n if (candidateFact !== null) {\n await semantic.supersede(conflictingId, candidateFact, judge.reason);\n factsUpdated += 1;\n }\n }\n await conflicts.markResolved(row.id, 'supersede');\n } else if (judge.decision === 'dedup' && conflictingId !== null) {\n // memory-consolidation-01: dedup on an LLM verdict must be a\n // SOFT forget (replayable tombstone), never the GDPR\n // hard-delete `purge` - \"never a destructive delete\" (P0-3)\n // applies doubly to a background path acting on model output.\n if (typeof semantic.forget === 'function') {\n await semantic.forget(row.factId, judge.reason);\n }\n factsUpdated += 1;\n await conflicts.markResolved(row.id, 'dedup');\n } else {\n await conflicts.markResolved(row.id, 'admit');\n }\n resolved += 1;\n }\n\n const snapshot = deps.budget.snapshot();\n span.setAttributes({\n 'consolidator.duration_ms': Math.max(\n 0,\n (typeof deps.now === 'function' ? deps.now : Date.now)() - startedAt,\n ),\n 'consolidator.facts_extracted': factsUpdated,\n 'consolidator.budget_used_usd': totalCost,\n 'consolidator.deep.resolved': resolved,\n 'consolidator.deep.facts_updated': factsUpdated,\n 'consolidator.deep.tokens.total': totalTokens,\n 'consolidator.deep.cost.estimate.usd': totalCost,\n 'consolidator.budget.remaining.tokens': snapshot.tokensRemaining,\n 'consolidator.budget.remaining.usd': snapshot.costRemaining,\n });\n\n return {\n phase: 'deep',\n status: 'completed',\n factsCreated: 0,\n factsUpdated,\n conflictsResolved: resolved,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: totalTokens,\n llmCostUsd: totalCost,\n errorMessage: null,\n };\n },\n );\n}\n\nfunction buildJudgeRequest(\n deps: DeepPhaseDeps,\n candidateText: string,\n existingText: string | null,\n): ProviderRequest {\n const userBlock = [\n `Candidate fact: ${candidateText}`,\n existingText !== null ? `Existing fact: ${existingText}` : 'Existing fact: (unknown)',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content: userBlock }],\n systemMessage: JUDGE_PROMPT,\n temperature: 0,\n // MCON-14: per-call output cap - the verdict shape is tiny.\n maxTokens: 256,\n metadata: {\n userId: deps.scope.userId,\n ...(deps.scope.sessionId !== undefined ? { sessionId: deps.scope.sessionId } : {}),\n ...(deps.scope.agentId !== undefined ? { agentId: deps.scope.agentId } : {}),\n },\n outputType: { kind: 'structured' },\n };\n}\n\nfunction parseJudge(text: string | undefined): JudgeOutcome | null {\n if (text === undefined || text.length === 0) return null;\n const candidate = stripFence(text).trim();\n if (candidate.length === 0) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(candidate);\n } catch {\n const slice = sliceJsonObject(candidate);\n if (slice === null) return null;\n try {\n parsed = JSON.parse(slice);\n } catch {\n return null;\n }\n }\n if (parsed === null || typeof parsed !== 'object') return null;\n const obj = parsed as { decision?: unknown; reason?: unknown };\n const decision = obj.decision;\n if (decision !== 'supersede' && decision !== 'dedup' && decision !== 'admit') return null;\n const reason = typeof obj.reason === 'string' ? obj.reason : 'unspecified';\n return { decision, reason };\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction sliceJsonObject(text: string): string | null {\n const start = text.indexOf('{');\n const end = text.lastIndexOf('}');\n if (start < 0 || end < start) return null;\n return text.slice(start, end + 1);\n}\n\nfunction makeOutcome(\n status: PhaseOutcome['status'],\n overrides: Partial<PhaseOutcome>,\n): PhaseOutcome {\n return {\n phase: 'deep',\n status,\n factsCreated: 0,\n factsUpdated: 0,\n conflictsResolved: 0,\n episodesFormed: 0,\n insightsCreated: 0,\n noiseFilteredCount: 0,\n emptyExtractions: 0,\n llmTokensUsed: 0,\n llmCostUsd: null,\n errorMessage: null,\n ...overrides,\n };\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ declare const MAX_PROCEDURE_STEPS = 50;
9
9
  /** Upper bound on trajectory steps rendered into the induction prompt. */
10
10
  declare const MAX_TRAJECTORY_STEPS_SHOWN = 60;
11
11
  /**
12
- * One distilled step of an agent trajectory model-agnostic, so the
12
+ * One distilled step of an agent trajectory - model-agnostic, so the
13
13
  * inducer never sees raw provider/tool wire formats.
14
14
  *
15
15
  * @stable
@@ -23,7 +23,7 @@ interface TrajectoryStep {
23
23
  /**
24
24
  * The minimal trajectory an inducer needs: the goal, the ordered steps, and
25
25
  * whether the run succeeded. Induction fires on **success only** (AWM online
26
- * mode) {@link runWorkflowInduction} returns `null` for a failed run.
26
+ * mode) - {@link runWorkflowInduction} returns `null` for a failed run.
27
27
  *
28
28
  * @stable
29
29
  */
@@ -32,7 +32,7 @@ interface Trajectory {
32
32
  readonly goal: string;
33
33
  /** Ordered steps the agent took. */
34
34
  readonly steps: ReadonlyArray<TrajectoryStep>;
35
- /** Whether the run succeeded the induction gate. */
35
+ /** Whether the run succeeded - the induction gate. */
36
36
  readonly succeeded: boolean;
37
37
  }
38
38
  /**
@@ -58,7 +58,7 @@ interface WorkflowInductionOptions {
58
58
  }
59
59
  /**
60
60
  * Provider-agnostic seam: turn one successful trajectory into a procedure.
61
- * Returns `null` when nothing inducible (degraded provider, empty output)
61
+ * Returns `null` when nothing inducible (degraded provider, empty output) -
62
62
  * never throws.
63
63
  *
64
64
  * @stable
@@ -82,7 +82,7 @@ declare function buildInductionRequest(trajectory: Trajectory, options?: Workflo
82
82
  /**
83
83
  * Parse the induction response into an {@link InducedProcedure}, tolerating
84
84
  * chatty / fenced output. Returns `null` when absent / unparseable / it
85
- * yields no steps. **Never throws on model output** only normalizes.
85
+ * yields no steps. **Never throws on model output** - only normalizes.
86
86
  *
87
87
  * The returned `variables` are reconciled with the steps: every `{name}`
88
88
  * placeholder actually present in the steps is guaranteed to appear, so the
@@ -108,12 +108,12 @@ declare function normalizeInducedProcedure(raw: {
108
108
  }): InducedProcedure | null;
109
109
  /**
110
110
  * Resilient provider-backed inducer. A provider throw or unparseable output
111
- * degrades to `null` (no procedure) induction never breaks the write path.
111
+ * degrades to `null` (no procedure) - induction never breaks the write path.
112
112
  */
113
113
  declare function createProviderWorkflowInducer(provider: Provider, options?: {
114
114
  readonly maxTokens?: number;
115
115
  /**
116
- * Usage callback (MCON-15) induction is the framework's highest
116
+ * Usage callback (MCON-15) - induction is the framework's highest
117
117
  * poisoning-risk LLM spend and previously flowed past every budget
118
118
  * envelope. `createMemory` wires this into the consolidator budget
119
119
  * when one is enabled; standalone callers can record it themselves.
@@ -127,14 +127,14 @@ declare function createProviderWorkflowInducer(provider: Provider, options?: {
127
127
  /**
128
128
  * Pure orchestrator: induce a procedure from a trajectory.
129
129
  *
130
- * **Gate successful trajectories only** (AWM online mode): a failed /
130
+ * **Gate - successful trajectories only** (AWM online mode): a failed /
131
131
  * aborted run, or one with no steps, yields `null` without calling the
132
132
  * inducer. Otherwise the inducer runs and the result is normalized.
133
133
  */
134
134
  declare function runWorkflowInduction(trajectory: Trajectory, inducer: WorkflowInducer, options?: WorkflowInductionOptions): Promise<InducedProcedure | null>;
135
135
  /**
136
136
  * Distil a {@link RunState} into the minimal {@link Trajectory} the inducer
137
- * needs. Pure consumes the agent's already-emitted run state, so capturing
137
+ * needs. Pure - consumes the agent's already-emitted run state, so capturing
138
138
  * a trajectory + its success signal needs no agent-loop change. The success
139
139
  * signal is `status === 'completed'`.
140
140
  */
@@ -142,7 +142,7 @@ declare function trajectoryFromRunState(run: RunState): Trajectory;
142
142
  /**
143
143
  * Self-verify a reuse against an induced procedure's success criteria. A
144
144
  * criterion is met when any observed signal contains it (case-insensitive
145
- * substring) a deterministic, offline check the agent runtime can feed
145
+ * substring) - a deterministic, offline check the agent runtime can feed
146
146
  * actual run observations into on reuse. With **no criteria**, the reuse
147
147
  * cannot be self-verified, so `verified` is `false`.
148
148
  */
@@ -12,7 +12,7 @@ const INDUCTION_SYSTEM_PROMPT = [
12
12
  "sequence of steps an agent took to SUCCEED at a task, distil a reusable procedure a future",
13
13
  "agent could follow for the same kind of task. Abstract concrete values into {snake_case}",
14
14
  "variables (e.g. \"dry cat food\" → {product}, \"Monday\" → {day}) so the procedure generalizes;",
15
- "keep the steps short and imperative. Also state a few verifiable success criteria concrete,",
15
+ "keep the steps short and imperative. Also state a few verifiable success criteria - concrete,",
16
16
  "checkable conditions that indicate the task is done. Do NOT invent steps that did not occur.",
17
17
  "Return strictly JSON: { \"title\": string, \"steps\": string[], \"variables\": string[],",
18
18
  "\"successCriteria\": string[] }."
@@ -48,7 +48,7 @@ function buildInductionRequest(trajectory, options = {}) {
48
48
  /**
49
49
  * Parse the induction response into an {@link InducedProcedure}, tolerating
50
50
  * chatty / fenced output. Returns `null` when absent / unparseable / it
51
- * yields no steps. **Never throws on model output** only normalizes.
51
+ * yields no steps. **Never throws on model output** - only normalizes.
52
52
  *
53
53
  * The returned `variables` are reconciled with the steps: every `{name}`
54
54
  * placeholder actually present in the steps is guaranteed to appear, so the
@@ -92,7 +92,7 @@ function normalizeInducedProcedure(raw) {
92
92
  }
93
93
  /**
94
94
  * Resilient provider-backed inducer. A provider throw or unparseable output
95
- * degrades to `null` (no procedure) induction never breaks the write path.
95
+ * degrades to `null` (no procedure) - induction never breaks the write path.
96
96
  */
97
97
  function createProviderWorkflowInducer(provider, options = {}) {
98
98
  return { async induce(trajectory, callOptions = {}) {
@@ -117,7 +117,7 @@ function createProviderWorkflowInducer(provider, options = {}) {
117
117
  /**
118
118
  * Pure orchestrator: induce a procedure from a trajectory.
119
119
  *
120
- * **Gate successful trajectories only** (AWM online mode): a failed /
120
+ * **Gate - successful trajectories only** (AWM online mode): a failed /
121
121
  * aborted run, or one with no steps, yields `null` without calling the
122
122
  * inducer. Otherwise the inducer runs and the result is normalized.
123
123
  */
@@ -135,7 +135,7 @@ async function runWorkflowInduction(trajectory, inducer, options = {}) {
135
135
  }
136
136
  /**
137
137
  * Distil a {@link RunState} into the minimal {@link Trajectory} the inducer
138
- * needs. Pure consumes the agent's already-emitted run state, so capturing
138
+ * needs. Pure - consumes the agent's already-emitted run state, so capturing
139
139
  * a trajectory + its success signal needs no agent-loop change. The success
140
140
  * signal is `status === 'completed'`.
141
141
  */
@@ -157,7 +157,7 @@ function trajectoryFromRunState(run) {
157
157
  /**
158
158
  * Self-verify a reuse against an induced procedure's success criteria. A
159
159
  * criterion is met when any observed signal contains it (case-insensitive
160
- * substring) a deterministic, offline check the agent runtime can feed
160
+ * substring) - a deterministic, offline check the agent runtime can feed
161
161
  * actual run observations into on reuse. With **no criteria**, the reuse
162
162
  * cannot be self-verified, so `verified` is `false`.
163
163
  */
@@ -1 +1 @@
1
- {"version":3,"file":"induce.js","names":["merged: WorkflowInductionOptions","steps: TrajectoryStep[]","out: string[]","parts: string[]"],"sources":["../../../src/consolidator/phases/induce.ts"],"sourcesContent":["/**\n * Procedural memory extraction (P2-2) — AWM-style workflow induction. From\n * a **successful** agent trajectory, distil a reusable procedure: a goal, a\n * value-abstracted step sequence (`\"search for {product}\"`), the variable\n * names that abstraction introduced, and Voyager-style success criteria the\n * reuse can self-verify against. The induced procedure is stored in the\n * procedural tier **quarantined** + `provenance: 'induction'` (P1-4) —\n * procedures drive *actions*, so this is the highest-poisoning-risk write in\n * the system and must never bypass the quarantine gate.\n *\n * Provider-agnostic seam, mirroring the P2-3 / P2-4 retrieval seams: pure\n * request builder ({@link buildInductionRequest}) + tolerant pure parser\n * ({@link parseInducedProcedure}) + a resilient provider-backed inducer\n * ({@link createProviderWorkflowInducer}) + a pure orchestrator\n * ({@link runWorkflowInduction}) that does no I/O of its own. The module\n * imports only `@graphorin/core` types, so the default (no-inducer) path is\n * fully offline — induction only ever calls a model when a provider is wired\n * via `createMemory({ procedureInduction: { provider } })`.\n *\n * \"Capture the trajectory + success signal\" (the proposal's agent-side\n * dependency) needs **no agent change**: the agent already emits the full,\n * serializable {@link RunState} (`steps[]` + `messages[]` + `status`).\n * {@link trajectoryFromRunState} distils it into the minimal\n * {@link Trajectory} the inducer consumes — `succeeded = status ===\n * 'completed'` is the AWM online-mode success signal (no ground truth\n * needed).\n *\n * @packageDocumentation\n */\n\nimport type { Message, Provider, ProviderRequest, RunState } from '@graphorin/core';\n\n/** Output-token ceiling for one induction call. */\nexport const DEFAULT_INDUCTION_MAX_TOKENS = 512;\n\n/** Hard cap on steps in an induced procedure (defends against runaway output). */\nexport const MAX_PROCEDURE_STEPS = 50;\n\n/** Upper bound on trajectory steps rendered into the induction prompt. */\nexport const MAX_TRAJECTORY_STEPS_SHOWN = 60;\n\n/** Matches `{snake_case}` / `{kebab-case}` variable placeholders in a step. */\nconst VARIABLE_PLACEHOLDER = /\\{([a-zA-Z][\\w-]*)\\}/gu;\n\n/**\n * One distilled step of an agent trajectory — model-agnostic, so the\n * inducer never sees raw provider/tool wire formats.\n *\n * @stable\n */\nexport interface TrajectoryStep {\n /** Tool invoked at this step, when the step was a tool call. */\n readonly tool?: string;\n /** Concrete args / observation rendered to text (the value-abstraction source). */\n readonly detail?: string;\n}\n\n/**\n * The minimal trajectory an inducer needs: the goal, the ordered steps, and\n * whether the run succeeded. Induction fires on **success only** (AWM online\n * mode) — {@link runWorkflowInduction} returns `null` for a failed run.\n *\n * @stable\n */\nexport interface Trajectory {\n /** What the run set out to do (typically the first user message). */\n readonly goal: string;\n /** Ordered steps the agent took. */\n readonly steps: ReadonlyArray<TrajectoryStep>;\n /** Whether the run succeeded — the induction gate. */\n readonly succeeded: boolean;\n}\n\n/**\n * A reusable workflow distilled from a successful trajectory.\n *\n * @stable\n */\nexport interface InducedProcedure {\n /** Short imperative title / goal of the reusable workflow. */\n readonly title: string;\n /** Ordered, value-abstracted steps (`\"search for {product}\"`, …). */\n readonly steps: ReadonlyArray<string>;\n /** Variable names abstracted from concrete values (`\"product\"`, `\"day\"`). */\n readonly variables: ReadonlyArray<string>;\n /** Voyager-style verifiable success criteria for self-verification on reuse. */\n readonly successCriteria: ReadonlyArray<string>;\n}\n\n/** Per-call options for an induction request. */\nexport interface WorkflowInductionOptions {\n /** Output-token ceiling. Default {@link DEFAULT_INDUCTION_MAX_TOKENS}. */\n readonly maxTokens?: number;\n readonly signal?: AbortSignal;\n}\n\n/**\n * Provider-agnostic seam: turn one successful trajectory into a procedure.\n * Returns `null` when nothing inducible (degraded provider, empty output) —\n * never throws.\n *\n * @stable\n */\nexport interface WorkflowInducer {\n induce(\n trajectory: Trajectory,\n options?: WorkflowInductionOptions,\n ): Promise<InducedProcedure | null>;\n}\n\n/** Result of self-verifying a reuse against an induced procedure's criteria. */\nexport interface VerificationResult {\n /** True only when the procedure has criteria and every one is met. */\n readonly verified: boolean;\n /** Criteria not satisfied by the observed signals. */\n readonly unmet: ReadonlyArray<string>;\n}\n\nexport const INDUCTION_SYSTEM_PROMPT = [\n 'You are the workflow-induction step of a long-running agent memory. Given the goal and the',\n 'sequence of steps an agent took to SUCCEED at a task, distil a reusable procedure a future',\n 'agent could follow for the same kind of task. Abstract concrete values into {snake_case}',\n 'variables (e.g. \"dry cat food\" → {product}, \"Monday\" → {day}) so the procedure generalizes;',\n 'keep the steps short and imperative. Also state a few verifiable success criteria — concrete,',\n 'checkable conditions that indicate the task is done. Do NOT invent steps that did not occur.',\n 'Return strictly JSON: { \"title\": string, \"steps\": string[], \"variables\": string[],',\n '\"successCriteria\": string[] }.',\n].join(' ');\n\n/**\n * Build the (pure) induction request. Renders the trajectory as numbered\n * steps; caps the rendered steps to keep the prompt bounded.\n */\nexport function buildInductionRequest(\n trajectory: Trajectory,\n options: WorkflowInductionOptions = {},\n): ProviderRequest {\n const stepLines = trajectory.steps\n .slice(0, MAX_TRAJECTORY_STEPS_SHOWN)\n .map((step, i) => {\n const head = step.tool !== undefined && step.tool.length > 0 ? step.tool : 'step';\n return step.detail !== undefined && step.detail.length > 0\n ? `[${i + 1}] ${head}: ${step.detail}`\n : `[${i + 1}] ${head}`;\n })\n .join('\\n');\n const content = [\n `Goal: ${trajectory.goal.length > 0 ? trajectory.goal : '(unstated)'}`,\n '',\n 'Trajectory (the steps the agent took to succeed):',\n stepLines.length > 0 ? stepLines : '(none)',\n '',\n 'Distil the reusable procedure.',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content }],\n systemMessage: INDUCTION_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Parse the induction response into an {@link InducedProcedure}, tolerating\n * chatty / fenced output. Returns `null` when absent / unparseable / it\n * yields no steps. **Never throws on model output** — only normalizes.\n *\n * The returned `variables` are reconciled with the steps: every `{name}`\n * placeholder actually present in the steps is guaranteed to appear, so the\n * abstraction is grounded in the procedure rather than in a model-declared\n * list that may drift from it.\n *\n * @internal\n */\nexport function parseInducedProcedure(text: string | undefined): InducedProcedure | null {\n if (text === undefined || text.length === 0) return null;\n const parsed = tryParseObject(stripFence(text).trim());\n if (!isRecord(parsed)) return null;\n return normalizeInducedProcedure({\n title: typeof parsed.title === 'string' ? parsed.title : '',\n steps: toStringArray(parsed.steps),\n variables: toStringArray(parsed.variables),\n successCriteria: toStringArray(parsed.successCriteria ?? parsed.success_criteria),\n });\n}\n\n/**\n * Normalize a raw induced procedure: trim + drop empty entries, cap step\n * count, and reconcile the variable list with the placeholders actually used\n * in the steps. Returns `null` when no usable steps remain (nothing to\n * induce).\n *\n * @internal\n */\nexport function normalizeInducedProcedure(raw: {\n title: string;\n steps: ReadonlyArray<string>;\n variables: ReadonlyArray<string>;\n successCriteria: ReadonlyArray<string>;\n}): InducedProcedure | null {\n const steps = raw.steps\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n .slice(0, MAX_PROCEDURE_STEPS);\n if (steps.length === 0) return null;\n\n // Variables = placeholders actually in the steps (source of truth, keeps\n // the abstraction grounded), unioned with any extra model-declared names.\n const fromSteps = extractPlaceholders(steps);\n const declared = raw.variables.map((v) => normalizeVariableName(v)).filter((v) => v.length > 0);\n const variables = dedupe([...fromSteps, ...declared]);\n\n const successCriteria = dedupe(\n raw.successCriteria.map((c) => c.trim()).filter((c) => c.length > 0),\n );\n const title = raw.title.trim().length > 0 ? raw.title.trim() : (steps[0] ?? 'induced procedure');\n return { title, steps, variables, successCriteria };\n}\n\n/**\n * Resilient provider-backed inducer. A provider throw or unparseable output\n * degrades to `null` (no procedure) — induction never breaks the write path.\n */\nexport function createProviderWorkflowInducer(\n provider: Provider,\n options: {\n readonly maxTokens?: number;\n /**\n * Usage callback (MCON-15) — induction is the framework's highest\n * poisoning-risk LLM spend and previously flowed past every budget\n * envelope. `createMemory` wires this into the consolidator budget\n * when one is enabled; standalone callers can record it themselves.\n * Best-effort: a throwing callback never breaks induction.\n */\n readonly onUsage?: (usage: {\n readonly promptTokens: number;\n readonly completionTokens: number;\n }) => void;\n } = {},\n): WorkflowInducer {\n return {\n async induce(trajectory, callOptions = {}): Promise<InducedProcedure | null> {\n const merged: WorkflowInductionOptions = {\n maxTokens: callOptions.maxTokens ?? options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,\n ...(callOptions.signal !== undefined ? { signal: callOptions.signal } : {}),\n };\n try {\n const res = await provider.generate(buildInductionRequest(trajectory, merged));\n try {\n options.onUsage?.({\n promptTokens: res.usage.promptTokens,\n completionTokens: res.usage.completionTokens,\n });\n } catch {\n // Accounting is advisory; never break the induction result.\n }\n return parseInducedProcedure(res.text);\n } catch {\n return null;\n }\n },\n };\n}\n\n/**\n * Pure orchestrator: induce a procedure from a trajectory.\n *\n * **Gate — successful trajectories only** (AWM online mode): a failed /\n * aborted run, or one with no steps, yields `null` without calling the\n * inducer. Otherwise the inducer runs and the result is normalized.\n */\nexport async function runWorkflowInduction(\n trajectory: Trajectory,\n inducer: WorkflowInducer,\n options: WorkflowInductionOptions = {},\n): Promise<InducedProcedure | null> {\n if (!trajectory.succeeded) return null;\n if (trajectory.steps.length === 0) return null;\n const induced = await inducer.induce(trajectory, options);\n if (induced === null) return null;\n return normalizeInducedProcedure({\n title: induced.title,\n steps: induced.steps,\n variables: induced.variables,\n successCriteria: induced.successCriteria,\n });\n}\n\n/**\n * Distil a {@link RunState} into the minimal {@link Trajectory} the inducer\n * needs. Pure — consumes the agent's already-emitted run state, so capturing\n * a trajectory + its success signal needs no agent-loop change. The success\n * signal is `status === 'completed'`.\n */\nexport function trajectoryFromRunState(run: RunState): Trajectory {\n const steps: TrajectoryStep[] = [];\n for (const step of run.steps) {\n for (const tc of step.toolCalls) {\n const detail = renderArgs(tc.call.args);\n steps.push({\n tool: tc.call.toolName,\n ...(detail !== undefined ? { detail } : {}),\n });\n }\n }\n return {\n goal: firstUserText(run.messages) ?? '',\n steps,\n succeeded: run.status === 'completed',\n };\n}\n\n/**\n * Self-verify a reuse against an induced procedure's success criteria. A\n * criterion is met when any observed signal contains it (case-insensitive\n * substring) — a deterministic, offline check the agent runtime can feed\n * actual run observations into on reuse. With **no criteria**, the reuse\n * cannot be self-verified, so `verified` is `false`.\n */\nexport function checkSuccessCriteria(\n procedure: { readonly successCriteria?: ReadonlyArray<string> },\n observed: ReadonlyArray<string>,\n): VerificationResult {\n const criteria = (procedure.successCriteria ?? [])\n .map((c) => c.trim())\n .filter((c) => c.length > 0);\n if (criteria.length === 0) return { verified: false, unmet: [] };\n const haystack = observed.map((o) => o.toLowerCase());\n const unmet = criteria.filter((c) => !haystack.some((o) => o.includes(c.toLowerCase())));\n return { verified: unmet.length === 0, unmet };\n}\n\n// ── helpers ──────────────────────────────────────────────────────────────\n\nfunction extractPlaceholders(steps: ReadonlyArray<string>): string[] {\n const out: string[] = [];\n for (const step of steps) {\n for (const match of step.matchAll(VARIABLE_PLACEHOLDER)) {\n const name = match[1];\n if (name !== undefined) out.push(name);\n }\n }\n return out;\n}\n\nfunction normalizeVariableName(value: string): string {\n // Accept either a bare name or a `{name}` placeholder.\n const inner = /^\\{([a-zA-Z][\\w-]*)\\}$/u.exec(value.trim());\n return (inner?.[1] ?? value).trim();\n}\n\nfunction dedupe(values: ReadonlyArray<string>): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const v of values) {\n if (seen.has(v)) continue;\n seen.add(v);\n out.push(v);\n }\n return out;\n}\n\nfunction renderArgs(args: unknown): string | undefined {\n if (args === undefined || args === null) return undefined;\n if (typeof args === 'string') return args.length > 0 ? truncate(args) : undefined;\n try {\n const json = JSON.stringify(args);\n return json !== undefined && json !== '{}' ? truncate(json) : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction truncate(value: string, max = 200): string {\n return value.length > max ? `${value.slice(0, max)}…` : value;\n}\n\nfunction firstUserText(messages: ReadonlyArray<Message>): string | undefined {\n for (const message of messages) {\n if (message.role !== 'user') continue;\n const text = messageText(message);\n if (text.length > 0) return text;\n }\n return undefined;\n}\n\nfunction messageText(message: Message): string {\n const content = message.content;\n if (typeof content === 'string') return content.trim();\n const parts: string[] = [];\n for (const part of content) {\n if (part.type === 'text' && typeof part.text === 'string') parts.push(part.text);\n }\n return parts.join(' ').trim();\n}\n\nfunction toStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is string => typeof v === 'string');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction tryParseObject(candidate: string): unknown {\n if (candidate.length === 0) return undefined;\n try {\n return JSON.parse(candidate);\n } catch {\n const start = candidate.indexOf('{');\n const end = candidate.lastIndexOf('}');\n if (start < 0 || end < start) return undefined;\n try {\n return JSON.parse(candidate.slice(start, end + 1));\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"induce.js","names":["merged: WorkflowInductionOptions","steps: TrajectoryStep[]","out: string[]","parts: string[]"],"sources":["../../../src/consolidator/phases/induce.ts"],"sourcesContent":["/**\n * Procedural memory extraction (P2-2) - AWM-style workflow induction. From\n * a **successful** agent trajectory, distil a reusable procedure: a goal, a\n * value-abstracted step sequence (`\"search for {product}\"`), the variable\n * names that abstraction introduced, and Voyager-style success criteria the\n * reuse can self-verify against. The induced procedure is stored in the\n * procedural tier **quarantined** + `provenance: 'induction'` (P1-4) -\n * procedures drive *actions*, so this is the highest-poisoning-risk write in\n * the system and must never bypass the quarantine gate.\n *\n * Provider-agnostic seam, mirroring the P2-3 / P2-4 retrieval seams: pure\n * request builder ({@link buildInductionRequest}) + tolerant pure parser\n * ({@link parseInducedProcedure}) + a resilient provider-backed inducer\n * ({@link createProviderWorkflowInducer}) + a pure orchestrator\n * ({@link runWorkflowInduction}) that does no I/O of its own. The module\n * imports only `@graphorin/core` types, so the default (no-inducer) path is\n * fully offline - induction only ever calls a model when a provider is wired\n * via `createMemory({ procedureInduction: { provider } })`.\n *\n * \"Capture the trajectory + success signal\" (the proposal's agent-side\n * dependency) needs **no agent change**: the agent already emits the full,\n * serializable {@link RunState} (`steps[]` + `messages[]` + `status`).\n * {@link trajectoryFromRunState} distils it into the minimal\n * {@link Trajectory} the inducer consumes - `succeeded = status ===\n * 'completed'` is the AWM online-mode success signal (no ground truth\n * needed).\n *\n * @packageDocumentation\n */\n\nimport type { Message, Provider, ProviderRequest, RunState } from '@graphorin/core';\n\n/** Output-token ceiling for one induction call. */\nexport const DEFAULT_INDUCTION_MAX_TOKENS = 512;\n\n/** Hard cap on steps in an induced procedure (defends against runaway output). */\nexport const MAX_PROCEDURE_STEPS = 50;\n\n/** Upper bound on trajectory steps rendered into the induction prompt. */\nexport const MAX_TRAJECTORY_STEPS_SHOWN = 60;\n\n/** Matches `{snake_case}` / `{kebab-case}` variable placeholders in a step. */\nconst VARIABLE_PLACEHOLDER = /\\{([a-zA-Z][\\w-]*)\\}/gu;\n\n/**\n * One distilled step of an agent trajectory - model-agnostic, so the\n * inducer never sees raw provider/tool wire formats.\n *\n * @stable\n */\nexport interface TrajectoryStep {\n /** Tool invoked at this step, when the step was a tool call. */\n readonly tool?: string;\n /** Concrete args / observation rendered to text (the value-abstraction source). */\n readonly detail?: string;\n}\n\n/**\n * The minimal trajectory an inducer needs: the goal, the ordered steps, and\n * whether the run succeeded. Induction fires on **success only** (AWM online\n * mode) - {@link runWorkflowInduction} returns `null` for a failed run.\n *\n * @stable\n */\nexport interface Trajectory {\n /** What the run set out to do (typically the first user message). */\n readonly goal: string;\n /** Ordered steps the agent took. */\n readonly steps: ReadonlyArray<TrajectoryStep>;\n /** Whether the run succeeded - the induction gate. */\n readonly succeeded: boolean;\n}\n\n/**\n * A reusable workflow distilled from a successful trajectory.\n *\n * @stable\n */\nexport interface InducedProcedure {\n /** Short imperative title / goal of the reusable workflow. */\n readonly title: string;\n /** Ordered, value-abstracted steps (`\"search for {product}\"`, …). */\n readonly steps: ReadonlyArray<string>;\n /** Variable names abstracted from concrete values (`\"product\"`, `\"day\"`). */\n readonly variables: ReadonlyArray<string>;\n /** Voyager-style verifiable success criteria for self-verification on reuse. */\n readonly successCriteria: ReadonlyArray<string>;\n}\n\n/** Per-call options for an induction request. */\nexport interface WorkflowInductionOptions {\n /** Output-token ceiling. Default {@link DEFAULT_INDUCTION_MAX_TOKENS}. */\n readonly maxTokens?: number;\n readonly signal?: AbortSignal;\n}\n\n/**\n * Provider-agnostic seam: turn one successful trajectory into a procedure.\n * Returns `null` when nothing inducible (degraded provider, empty output) -\n * never throws.\n *\n * @stable\n */\nexport interface WorkflowInducer {\n induce(\n trajectory: Trajectory,\n options?: WorkflowInductionOptions,\n ): Promise<InducedProcedure | null>;\n}\n\n/** Result of self-verifying a reuse against an induced procedure's criteria. */\nexport interface VerificationResult {\n /** True only when the procedure has criteria and every one is met. */\n readonly verified: boolean;\n /** Criteria not satisfied by the observed signals. */\n readonly unmet: ReadonlyArray<string>;\n}\n\nexport const INDUCTION_SYSTEM_PROMPT = [\n 'You are the workflow-induction step of a long-running agent memory. Given the goal and the',\n 'sequence of steps an agent took to SUCCEED at a task, distil a reusable procedure a future',\n 'agent could follow for the same kind of task. Abstract concrete values into {snake_case}',\n 'variables (e.g. \"dry cat food\" → {product}, \"Monday\" → {day}) so the procedure generalizes;',\n 'keep the steps short and imperative. Also state a few verifiable success criteria - concrete,',\n 'checkable conditions that indicate the task is done. Do NOT invent steps that did not occur.',\n 'Return strictly JSON: { \"title\": string, \"steps\": string[], \"variables\": string[],',\n '\"successCriteria\": string[] }.',\n].join(' ');\n\n/**\n * Build the (pure) induction request. Renders the trajectory as numbered\n * steps; caps the rendered steps to keep the prompt bounded.\n */\nexport function buildInductionRequest(\n trajectory: Trajectory,\n options: WorkflowInductionOptions = {},\n): ProviderRequest {\n const stepLines = trajectory.steps\n .slice(0, MAX_TRAJECTORY_STEPS_SHOWN)\n .map((step, i) => {\n const head = step.tool !== undefined && step.tool.length > 0 ? step.tool : 'step';\n return step.detail !== undefined && step.detail.length > 0\n ? `[${i + 1}] ${head}: ${step.detail}`\n : `[${i + 1}] ${head}`;\n })\n .join('\\n');\n const content = [\n `Goal: ${trajectory.goal.length > 0 ? trajectory.goal : '(unstated)'}`,\n '',\n 'Trajectory (the steps the agent took to succeed):',\n stepLines.length > 0 ? stepLines : '(none)',\n '',\n 'Distil the reusable procedure.',\n ].join('\\n');\n return {\n messages: [{ role: 'user', content }],\n systemMessage: INDUCTION_SYSTEM_PROMPT,\n temperature: 0,\n maxTokens: options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n outputType: { kind: 'structured' },\n };\n}\n\n/**\n * Parse the induction response into an {@link InducedProcedure}, tolerating\n * chatty / fenced output. Returns `null` when absent / unparseable / it\n * yields no steps. **Never throws on model output** - only normalizes.\n *\n * The returned `variables` are reconciled with the steps: every `{name}`\n * placeholder actually present in the steps is guaranteed to appear, so the\n * abstraction is grounded in the procedure rather than in a model-declared\n * list that may drift from it.\n *\n * @internal\n */\nexport function parseInducedProcedure(text: string | undefined): InducedProcedure | null {\n if (text === undefined || text.length === 0) return null;\n const parsed = tryParseObject(stripFence(text).trim());\n if (!isRecord(parsed)) return null;\n return normalizeInducedProcedure({\n title: typeof parsed.title === 'string' ? parsed.title : '',\n steps: toStringArray(parsed.steps),\n variables: toStringArray(parsed.variables),\n successCriteria: toStringArray(parsed.successCriteria ?? parsed.success_criteria),\n });\n}\n\n/**\n * Normalize a raw induced procedure: trim + drop empty entries, cap step\n * count, and reconcile the variable list with the placeholders actually used\n * in the steps. Returns `null` when no usable steps remain (nothing to\n * induce).\n *\n * @internal\n */\nexport function normalizeInducedProcedure(raw: {\n title: string;\n steps: ReadonlyArray<string>;\n variables: ReadonlyArray<string>;\n successCriteria: ReadonlyArray<string>;\n}): InducedProcedure | null {\n const steps = raw.steps\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n .slice(0, MAX_PROCEDURE_STEPS);\n if (steps.length === 0) return null;\n\n // Variables = placeholders actually in the steps (source of truth, keeps\n // the abstraction grounded), unioned with any extra model-declared names.\n const fromSteps = extractPlaceholders(steps);\n const declared = raw.variables.map((v) => normalizeVariableName(v)).filter((v) => v.length > 0);\n const variables = dedupe([...fromSteps, ...declared]);\n\n const successCriteria = dedupe(\n raw.successCriteria.map((c) => c.trim()).filter((c) => c.length > 0),\n );\n const title = raw.title.trim().length > 0 ? raw.title.trim() : (steps[0] ?? 'induced procedure');\n return { title, steps, variables, successCriteria };\n}\n\n/**\n * Resilient provider-backed inducer. A provider throw or unparseable output\n * degrades to `null` (no procedure) - induction never breaks the write path.\n */\nexport function createProviderWorkflowInducer(\n provider: Provider,\n options: {\n readonly maxTokens?: number;\n /**\n * Usage callback (MCON-15) - induction is the framework's highest\n * poisoning-risk LLM spend and previously flowed past every budget\n * envelope. `createMemory` wires this into the consolidator budget\n * when one is enabled; standalone callers can record it themselves.\n * Best-effort: a throwing callback never breaks induction.\n */\n readonly onUsage?: (usage: {\n readonly promptTokens: number;\n readonly completionTokens: number;\n }) => void;\n } = {},\n): WorkflowInducer {\n return {\n async induce(trajectory, callOptions = {}): Promise<InducedProcedure | null> {\n const merged: WorkflowInductionOptions = {\n maxTokens: callOptions.maxTokens ?? options.maxTokens ?? DEFAULT_INDUCTION_MAX_TOKENS,\n ...(callOptions.signal !== undefined ? { signal: callOptions.signal } : {}),\n };\n try {\n const res = await provider.generate(buildInductionRequest(trajectory, merged));\n try {\n options.onUsage?.({\n promptTokens: res.usage.promptTokens,\n completionTokens: res.usage.completionTokens,\n });\n } catch {\n // Accounting is advisory; never break the induction result.\n }\n return parseInducedProcedure(res.text);\n } catch {\n return null;\n }\n },\n };\n}\n\n/**\n * Pure orchestrator: induce a procedure from a trajectory.\n *\n * **Gate - successful trajectories only** (AWM online mode): a failed /\n * aborted run, or one with no steps, yields `null` without calling the\n * inducer. Otherwise the inducer runs and the result is normalized.\n */\nexport async function runWorkflowInduction(\n trajectory: Trajectory,\n inducer: WorkflowInducer,\n options: WorkflowInductionOptions = {},\n): Promise<InducedProcedure | null> {\n if (!trajectory.succeeded) return null;\n if (trajectory.steps.length === 0) return null;\n const induced = await inducer.induce(trajectory, options);\n if (induced === null) return null;\n return normalizeInducedProcedure({\n title: induced.title,\n steps: induced.steps,\n variables: induced.variables,\n successCriteria: induced.successCriteria,\n });\n}\n\n/**\n * Distil a {@link RunState} into the minimal {@link Trajectory} the inducer\n * needs. Pure - consumes the agent's already-emitted run state, so capturing\n * a trajectory + its success signal needs no agent-loop change. The success\n * signal is `status === 'completed'`.\n */\nexport function trajectoryFromRunState(run: RunState): Trajectory {\n const steps: TrajectoryStep[] = [];\n for (const step of run.steps) {\n for (const tc of step.toolCalls) {\n const detail = renderArgs(tc.call.args);\n steps.push({\n tool: tc.call.toolName,\n ...(detail !== undefined ? { detail } : {}),\n });\n }\n }\n return {\n goal: firstUserText(run.messages) ?? '',\n steps,\n succeeded: run.status === 'completed',\n };\n}\n\n/**\n * Self-verify a reuse against an induced procedure's success criteria. A\n * criterion is met when any observed signal contains it (case-insensitive\n * substring) - a deterministic, offline check the agent runtime can feed\n * actual run observations into on reuse. With **no criteria**, the reuse\n * cannot be self-verified, so `verified` is `false`.\n */\nexport function checkSuccessCriteria(\n procedure: { readonly successCriteria?: ReadonlyArray<string> },\n observed: ReadonlyArray<string>,\n): VerificationResult {\n const criteria = (procedure.successCriteria ?? [])\n .map((c) => c.trim())\n .filter((c) => c.length > 0);\n if (criteria.length === 0) return { verified: false, unmet: [] };\n const haystack = observed.map((o) => o.toLowerCase());\n const unmet = criteria.filter((c) => !haystack.some((o) => o.includes(c.toLowerCase())));\n return { verified: unmet.length === 0, unmet };\n}\n\n// ── helpers ──────────────────────────────────────────────────────────────\n\nfunction extractPlaceholders(steps: ReadonlyArray<string>): string[] {\n const out: string[] = [];\n for (const step of steps) {\n for (const match of step.matchAll(VARIABLE_PLACEHOLDER)) {\n const name = match[1];\n if (name !== undefined) out.push(name);\n }\n }\n return out;\n}\n\nfunction normalizeVariableName(value: string): string {\n // Accept either a bare name or a `{name}` placeholder.\n const inner = /^\\{([a-zA-Z][\\w-]*)\\}$/u.exec(value.trim());\n return (inner?.[1] ?? value).trim();\n}\n\nfunction dedupe(values: ReadonlyArray<string>): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const v of values) {\n if (seen.has(v)) continue;\n seen.add(v);\n out.push(v);\n }\n return out;\n}\n\nfunction renderArgs(args: unknown): string | undefined {\n if (args === undefined || args === null) return undefined;\n if (typeof args === 'string') return args.length > 0 ? truncate(args) : undefined;\n try {\n const json = JSON.stringify(args);\n return json !== undefined && json !== '{}' ? truncate(json) : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction truncate(value: string, max = 200): string {\n return value.length > max ? `${value.slice(0, max)}…` : value;\n}\n\nfunction firstUserText(messages: ReadonlyArray<Message>): string | undefined {\n for (const message of messages) {\n if (message.role !== 'user') continue;\n const text = messageText(message);\n if (text.length > 0) return text;\n }\n return undefined;\n}\n\nfunction messageText(message: Message): string {\n const content = message.content;\n if (typeof content === 'string') return content.trim();\n const parts: string[] = [];\n for (const part of content) {\n if (part.type === 'text' && typeof part.text === 'string') parts.push(part.text);\n }\n return parts.join(' ').trim();\n}\n\nfunction toStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.filter((v): v is string => typeof v === 'string');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction stripFence(text: string): string {\n const match = /^```[^\\n]*\\n([\\s\\S]*?)\\n```/u.exec(text.trim());\n return match?.[1] ?? text;\n}\n\nfunction tryParseObject(candidate: string): unknown {\n if (candidate.length === 0) return undefined;\n try {\n return JSON.parse(candidate);\n } catch {\n const start = candidate.indexOf('{');\n const end = candidate.lastIndexOf('}');\n if (start < 0 || end < start) return undefined;\n try {\n return JSON.parse(candidate.slice(start, end + 1));\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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"}