open-multi-agent-kit 0.90.3 → 0.90.4

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 (171) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +16 -14
  3. package/dist/config.d.ts +3 -6
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +27 -19
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/adaptorch-bridge.d.ts +257 -0
  8. package/dist/core/adaptorch-bridge.d.ts.map +1 -0
  9. package/dist/core/adaptorch-bridge.js +473 -0
  10. package/dist/core/adaptorch-bridge.js.map +1 -0
  11. package/dist/core/agent-session.d.ts +130 -0
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +286 -0
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/bang-skill-invocation.d.ts.map +1 -1
  16. package/dist/core/bang-skill-invocation.js +149 -0
  17. package/dist/core/bang-skill-invocation.js.map +1 -1
  18. package/dist/core/compaction/utils.d.ts.map +1 -1
  19. package/dist/core/compaction/utils.js +15 -2
  20. package/dist/core/compaction/utils.js.map +1 -1
  21. package/dist/core/domain-loadouts.d.ts.map +1 -1
  22. package/dist/core/domain-loadouts.js +1 -1
  23. package/dist/core/domain-loadouts.js.map +1 -1
  24. package/dist/core/reasoning-router-bias.d.ts +119 -0
  25. package/dist/core/reasoning-router-bias.d.ts.map +1 -0
  26. package/dist/core/reasoning-router-bias.js +287 -0
  27. package/dist/core/reasoning-router-bias.js.map +1 -0
  28. package/dist/core/reasoning-router-v2.d.ts +94 -0
  29. package/dist/core/reasoning-router-v2.d.ts.map +1 -0
  30. package/dist/core/reasoning-router-v2.js +288 -0
  31. package/dist/core/reasoning-router-v2.js.map +1 -0
  32. package/dist/core/reasoning-router-v3.d.ts +15 -0
  33. package/dist/core/reasoning-router-v3.d.ts.map +1 -0
  34. package/dist/core/reasoning-router-v3.js +193 -0
  35. package/dist/core/reasoning-router-v3.js.map +1 -0
  36. package/dist/core/reasoning-router-v4-weights.d.ts +124 -0
  37. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -0
  38. package/dist/core/reasoning-router-v4-weights.js +112 -0
  39. package/dist/core/reasoning-router-v4-weights.js.map +1 -0
  40. package/dist/core/reasoning-router-v4.d.ts +163 -0
  41. package/dist/core/reasoning-router-v4.d.ts.map +1 -0
  42. package/dist/core/reasoning-router-v4.js +683 -0
  43. package/dist/core/reasoning-router-v4.js.map +1 -0
  44. package/dist/core/reasoning-router-weights.d.ts +155 -0
  45. package/dist/core/reasoning-router-weights.d.ts.map +1 -0
  46. package/dist/core/reasoning-router-weights.js +204 -0
  47. package/dist/core/reasoning-router-weights.js.map +1 -0
  48. package/dist/core/reasoning-router.d.ts +44 -0
  49. package/dist/core/reasoning-router.d.ts.map +1 -0
  50. package/dist/core/reasoning-router.js +138 -0
  51. package/dist/core/reasoning-router.js.map +1 -0
  52. package/dist/core/router-feedback-collector.d.ts +101 -0
  53. package/dist/core/router-feedback-collector.d.ts.map +1 -0
  54. package/dist/core/router-feedback-collector.js +239 -0
  55. package/dist/core/router-feedback-collector.js.map +1 -0
  56. package/dist/core/settings-manager.d.ts +28 -0
  57. package/dist/core/settings-manager.d.ts.map +1 -1
  58. package/dist/core/settings-manager.js +16 -0
  59. package/dist/core/settings-manager.js.map +1 -1
  60. package/dist/core/slash-commands.d.ts.map +1 -1
  61. package/dist/core/slash-commands.js +4 -1
  62. package/dist/core/slash-commands.js.map +1 -1
  63. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +15 -0
  64. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/control-panel-runtime-status.js +95 -5
  66. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  68. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.js +143 -18
  70. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  71. package/docs/loadout-domains/README.md +8 -6
  72. package/docs/loadout-domains/ai-agent-ops.md +3 -2
  73. package/docs/loadout-domains/backend-api.md +3 -2
  74. package/docs/loadout-domains/data-science.md +3 -2
  75. package/docs/loadout-domains/devops-infra.md +2 -1
  76. package/docs/loadout-domains/docs-writing.md +7 -2
  77. package/docs/loadout-domains/frontend-ui.md +42 -11
  78. package/docs/loadout-domains/korean-document.md +83 -0
  79. package/docs/loadout-domains/mobile.md +1 -1
  80. package/docs/loadout-domains/qa-testing.md +4 -2
  81. package/docs/loadout-domains/security-audit.md +2 -1
  82. package/docs/loadout-domains/visual-qa.md +94 -0
  83. package/docs/skills.md +4 -1
  84. package/docs/usage.md +37 -3
  85. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  86. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  87. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  88. package/examples/extensions/gondolin/package-lock.json +2 -2
  89. package/examples/extensions/gondolin/package.json +1 -1
  90. package/examples/extensions/sandbox/package-lock.json +2 -2
  91. package/examples/extensions/sandbox/package.json +1 -1
  92. package/examples/extensions/with-deps/package-lock.json +2 -2
  93. package/examples/extensions/with-deps/package.json +1 -1
  94. package/npm-shrinkwrap.json +12 -12
  95. package/package.json +4 -4
  96. package/dist/agents/jailbreak/index.d.ts +0 -7
  97. package/dist/agents/jailbreak/index.d.ts.map +0 -1
  98. package/dist/agents/jailbreak/index.js +0 -7
  99. package/dist/agents/jailbreak/index.js.map +0 -1
  100. package/dist/agents/jailbreak/subagent-f.d.ts +0 -44
  101. package/dist/agents/jailbreak/subagent-f.d.ts.map +0 -1
  102. package/dist/agents/jailbreak/subagent-f.js +0 -60
  103. package/dist/agents/jailbreak/subagent-f.js.map +0 -1
  104. package/dist/cli/jailbreak-args.d.ts +0 -26
  105. package/dist/cli/jailbreak-args.d.ts.map +0 -1
  106. package/dist/cli/jailbreak-args.js +0 -86
  107. package/dist/cli/jailbreak-args.js.map +0 -1
  108. package/dist/commands/jailbreak.d.ts +0 -49
  109. package/dist/commands/jailbreak.d.ts.map +0 -1
  110. package/dist/commands/jailbreak.js +0 -201
  111. package/dist/commands/jailbreak.js.map +0 -1
  112. package/dist/encoding/index.d.ts +0 -6
  113. package/dist/encoding/index.d.ts.map +0 -1
  114. package/dist/encoding/index.js +0 -6
  115. package/dist/encoding/index.js.map +0 -1
  116. package/dist/encoding/l4-encoder.d.ts +0 -96
  117. package/dist/encoding/l4-encoder.d.ts.map +0 -1
  118. package/dist/encoding/l4-encoder.js +0 -474
  119. package/dist/encoding/l4-encoder.js.map +0 -1
  120. package/dist/fuzzing/index.d.ts +0 -6
  121. package/dist/fuzzing/index.d.ts.map +0 -1
  122. package/dist/fuzzing/index.js +0 -6
  123. package/dist/fuzzing/index.js.map +0 -1
  124. package/dist/fuzzing/mutation-engine.d.ts +0 -93
  125. package/dist/fuzzing/mutation-engine.d.ts.map +0 -1
  126. package/dist/fuzzing/mutation-engine.js +0 -244
  127. package/dist/fuzzing/mutation-engine.js.map +0 -1
  128. package/dist/jailbreak-extension.d.ts +0 -48
  129. package/dist/jailbreak-extension.d.ts.map +0 -1
  130. package/dist/jailbreak-extension.js +0 -47
  131. package/dist/jailbreak-extension.js.map +0 -1
  132. package/dist/modules/index.d.ts +0 -8
  133. package/dist/modules/index.d.ts.map +0 -1
  134. package/dist/modules/index.js +0 -7
  135. package/dist/modules/index.js.map +0 -1
  136. package/dist/modules/lrl-bypass.d.ts +0 -40
  137. package/dist/modules/lrl-bypass.d.ts.map +0 -1
  138. package/dist/modules/lrl-bypass.js +0 -84
  139. package/dist/modules/lrl-bypass.js.map +0 -1
  140. package/dist/modules/lrl-deps.d.ts +0 -38
  141. package/dist/modules/lrl-deps.d.ts.map +0 -1
  142. package/dist/modules/lrl-deps.js +0 -128
  143. package/dist/modules/lrl-deps.js.map +0 -1
  144. package/dist/multiturn/easl-chain.d.ts +0 -113
  145. package/dist/multiturn/easl-chain.d.ts.map +0 -1
  146. package/dist/multiturn/easl-chain.js +0 -351
  147. package/dist/multiturn/easl-chain.js.map +0 -1
  148. package/dist/multiturn/index.d.ts +0 -6
  149. package/dist/multiturn/index.d.ts.map +0 -1
  150. package/dist/multiturn/index.js +0 -6
  151. package/dist/multiturn/index.js.map +0 -1
  152. package/dist/routing/fallback-router.d.ts +0 -123
  153. package/dist/routing/fallback-router.d.ts.map +0 -1
  154. package/dist/routing/fallback-router.js +0 -355
  155. package/dist/routing/fallback-router.js.map +0 -1
  156. package/dist/routing/index.d.ts +0 -6
  157. package/dist/routing/index.d.ts.map +0 -1
  158. package/dist/routing/index.js +0 -6
  159. package/dist/routing/index.js.map +0 -1
  160. package/dist/types/jailbreak.d.ts +0 -246
  161. package/dist/types/jailbreak.d.ts.map +0 -1
  162. package/dist/types/jailbreak.js +0 -8
  163. package/dist/types/jailbreak.js.map +0 -1
  164. package/dist/utils/jailbreak/common.d.ts +0 -63
  165. package/dist/utils/jailbreak/common.d.ts.map +0 -1
  166. package/dist/utils/jailbreak/common.js +0 -130
  167. package/dist/utils/jailbreak/common.js.map +0 -1
  168. package/dist/utils/jailbreak/index.d.ts +0 -7
  169. package/dist/utils/jailbreak/index.d.ts.map +0 -1
  170. package/dist/utils/jailbreak/index.js +0 -7
  171. package/dist/utils/jailbreak/index.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-router-bias.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-bias.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,EAKN,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAE5B,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,MAAM,gCAAgC,CAAC;AAExC,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,2GAA2G;AAC3G,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAElD,8GAA4G;AAC5G,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,cAAe,SAAQ,iBAAiB;IACxD,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,aAAa,EAAE,yBAAyB,CAAC;IAClD,oGAAoG;IACpG,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,iGAAiG;IACjG,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iHAAiH;IACjH,QAAQ,CAAC,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,0BAA0B;IAC1C;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAgDD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,CA0DpB;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,GAAG,eAAe,CAMzG;AAwDD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAaxE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,kBAAkB,CAYhF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAQ9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,GAAE,qBAA4B,GAAG,MAAM,CAE9F","sourcesContent":["/**\n * Reasoning-router bias compiler — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).\n *\n * Pure, deterministic compiler from a set of privacy-safe feedback records\n * (see router-feedback-collector.ts) to a bounded, per-cell reasoning-level\n * bias snapshot. No clock, randomness, network, or model calls: the same\n * input array always produces the same output (`JSON.stringify` byte-\n * identical), regardless of input record order. This module is a standalone,\n * pure utility — nothing here is wired into a router resolver or\n * agent-session.ts by this lane.\n *\n * Goal 010 Lane I addition: `isRouterBiasCell` / `isRouterBiasSnapshot` /\n * `parseRouterBiasSnapshot` / `getDefaultRouterBiasSnapshotPath` below let a\n * (now opt-in) caller safely load a snapshot this module previously only\n * wrote. `parseRouterBiasSnapshot` performs no filesystem access itself — it\n * only validates already-read text — so this module's zero-I/O determinism\n * guarantee for `compileBiasSnapshot`/`getBiasStepsForCell` is unchanged; the\n * actual disk read lives in agent-session.ts.\n */\n\nimport { createHash } from \"crypto\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tisRouterFeedbackRecord,\n\tROUTER_FEEDBACK_LANE_TYPES,\n\tROUTER_FEEDBACK_LEN_BUCKETS,\n\tROUTER_FEEDBACK_TASK_CLASSES,\n\ttype RouterFeedbackLaneType,\n\ttype RouterFeedbackLenBucket,\n\ttype RouterFeedbackRecord,\n\ttype RouterFeedbackTaskClass,\n\ttype RouterFeedbackVersion,\n} from \"./router-feedback-collector.ts\";\n\n/** Minimum non-neutral (\"strong\") record count per cell before any bias applies. */\nexport const BIAS_STRONG_THRESHOLD = 5;\n\n/** Bias magnitude bound, matching the resolver's existing bias clamp (reasoning-router-v2.ts BIAS_MAX). */\nexport const BIAS_MAX_STEPS = 2;\n\n/** Bounded per-cell bias, in ladder steps. Never outside [-2, 2]. */\nexport type RouterBiasSteps = -2 | -1 | 0 | 1 | 2;\n\n/** The feature tuple a bias cell is keyed on — identical dimensions the router already computes locally. */\nexport interface RouterBiasCellKey {\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport interface RouterBiasCell extends RouterBiasCellKey {\n\treadonly biasSteps: RouterBiasSteps;\n\t/** Non-neutral (up/down-signaling) record count for this cell. */\n\treadonly nStrong: number;\n\t/** Total record count for this cell, including neutral (accepted/same/pass) records. */\n\treadonly nTotal: number;\n}\n\nexport interface RouterBiasSnapshot {\n\treadonly schemaVersion: \"router-bias-snapshot/v1\";\n\t/** sha256 hex digest of the canonicalized valid-record set actually compiled; not a prompt hash. */\n\treadonly sourceRecordDigest: string;\n\t/** Count of records that were validated and included in this snapshot. */\n\treadonly consideredCount: number;\n\t/** Count of records excluded from this snapshot (schema-invalid, or router-version mismatch). */\n\treadonly droppedCount: number;\n\t/** Bias cells, sorted deterministically. Empty when there is no ledger data or no cell reaches the threshold. */\n\treadonly biasCells: readonly RouterBiasCell[];\n}\n\nexport interface CompileBiasSnapshotOptions {\n\t/**\n\t * Restrict compilation to one router version — `compileBiasSnapshot` never\n\t * mixes records from different router versions into one snapshot. Records\n\t * tagged with any other `routerVersion` are excluded from this snapshot and\n\t * counted in `droppedCount`. Defaults to \"v3\" for API compatibility; the\n\t * offline CLI now defaults to \"v4\", matching the only auto-router wired to\n\t * consume a learning-bias snapshot at runtime.\n\t */\n\treadonly routerVersion?: RouterFeedbackVersion;\n}\n\nfunction cellKeyString(key: RouterBiasCellKey): string {\n\treturn [key.predictedClass, key.laneType, String(key.lenBucket), String(key.hadFence), String(key.hadDiff)].join(\n\t\t\"|\",\n\t);\n}\n\n/** +1 \"needed more effort\", -1 \"needed less effort\", 0 neutral (no directional bias contribution). */\nfunction outcomeDirection(outcome: RouterFeedbackRecord[\"outcome\"]): -1 | 0 | 1 {\n\tif (outcome === \"up\" || outcome === \"fail\" || outcome === \"debug-follow-up\") return 1;\n\tif (outcome === \"down\") return -1;\n\treturn 0;\n}\n\n/** Fixed-key-order JSON so identical records always canonicalize to identical bytes. */\nfunction canonicalRecordJson(record: RouterFeedbackRecord): string {\n\treturn JSON.stringify({\n\t\trouterVersion: record.routerVersion,\n\t\tlaneType: record.laneType,\n\t\tpredictedClass: record.predictedClass,\n\t\tresolvedLevel: record.resolvedLevel,\n\t\tacceptedLevel: record.acceptedLevel,\n\t\tsignal: record.signal,\n\t\toutcome: record.outcome,\n\t\tlenBucket: record.lenBucket,\n\t\thadFence: record.hadFence,\n\t\thadDiff: record.hadDiff,\n\t});\n}\n\ninterface CellAccumulator extends RouterBiasCellKey {\n\tnTotal: number;\n\tupVotes: number;\n\tdownVotes: number;\n}\n\nfunction computeBiasSteps(upVotes: number, downVotes: number, nStrong: number): RouterBiasSteps {\n\tif (nStrong < BIAS_STRONG_THRESHOLD) return 0;\n\tconst majority = upVotes - downVotes;\n\tif (majority === 0) return 0;\n\n\tconst dominance = Math.abs(majority) / nStrong;\n\tconst magnitude = dominance >= 0.75 ? BIAS_MAX_STEPS : 1;\n\tconst signedMagnitude = majority > 0 ? magnitude : -magnitude;\n\treturn Math.max(-BIAS_MAX_STEPS, Math.min(BIAS_MAX_STEPS, signedMagnitude)) as RouterBiasSteps;\n}\n\n/**\n * Deterministically compile a bounded bias snapshot from raw (possibly\n * malformed) ledger entries. Unknown-shaped entries — including ones with\n * extra keys, missing keys, or out-of-range enum values — are dropped and\n * counted, never thrown. Records tagged with a `routerVersion` other than the\n * target version are also dropped and counted (never merged across router\n * versions into one snapshot). An empty or all-dropped input compiles to an\n * empty-bias snapshot (`biasCells: []`), which is exactly the shipped\n * no-learning behavior — this makes \"empty ledger -> zero bias\" true by\n * construction, not by a special case.\n *\n * Pure: same `records` (any order) -> same `RouterBiasSnapshot` (byte-\n * identical `JSON.stringify` output), because cells are grouped by an\n * order-independent key and the emitted cell/record ordering is always\n * sorted before serialization.\n */\nexport function compileBiasSnapshot(\n\trecords: readonly unknown[],\n\toptions: CompileBiasSnapshotOptions = {},\n): RouterBiasSnapshot {\n\tconst routerVersion = options.routerVersion ?? \"v3\";\n\tconst valid: RouterFeedbackRecord[] = [];\n\tlet droppedCount = 0;\n\n\tfor (const entry of records) {\n\t\tif (isRouterFeedbackRecord(entry) && entry.routerVersion === routerVersion) {\n\t\t\tvalid.push(entry);\n\t\t} else {\n\t\t\tdroppedCount += 1;\n\t\t}\n\t}\n\n\tconst cells = new Map<string, CellAccumulator>();\n\tfor (const record of valid) {\n\t\tconst key: RouterBiasCellKey = {\n\t\t\tpredictedClass: record.predictedClass,\n\t\t\tlaneType: record.laneType,\n\t\t\tlenBucket: record.lenBucket,\n\t\t\thadFence: record.hadFence,\n\t\t\thadDiff: record.hadDiff,\n\t\t};\n\t\tconst mapKey = cellKeyString(key);\n\t\tconst existing = cells.get(mapKey) ?? { ...key, nTotal: 0, upVotes: 0, downVotes: 0 };\n\t\texisting.nTotal += 1;\n\t\tconst direction = outcomeDirection(record.outcome);\n\t\tif (direction === 1) existing.upVotes += 1;\n\t\telse if (direction === -1) existing.downVotes += 1;\n\t\tcells.set(mapKey, existing);\n\t}\n\n\tconst biasCells: RouterBiasCell[] = [];\n\tfor (const mapKey of [...cells.keys()].sort()) {\n\t\tconst acc = cells.get(mapKey);\n\t\tif (!acc) continue;\n\t\tconst nStrong = acc.upVotes + acc.downVotes;\n\t\tbiasCells.push({\n\t\t\tpredictedClass: acc.predictedClass,\n\t\t\tlaneType: acc.laneType,\n\t\t\tlenBucket: acc.lenBucket,\n\t\t\thadFence: acc.hadFence,\n\t\t\thadDiff: acc.hadDiff,\n\t\t\tbiasSteps: computeBiasSteps(acc.upVotes, acc.downVotes, nStrong),\n\t\t\tnStrong,\n\t\t\tnTotal: acc.nTotal,\n\t\t});\n\t}\n\n\tconst canonicalValid = valid.map(canonicalRecordJson).sort();\n\tconst sourceRecordDigest = createHash(\"sha256\").update(canonicalValid.join(\"\\n\")).digest(\"hex\");\n\n\treturn {\n\t\tschemaVersion: \"router-bias-snapshot/v1\",\n\t\tsourceRecordDigest,\n\t\tconsideredCount: valid.length,\n\t\tdroppedCount,\n\t\tbiasCells,\n\t};\n}\n\n/** Look up the compiled bias for one feature cell; 0 (no bias) when the cell is absent from the snapshot. */\nexport function getBiasStepsForCell(snapshot: RouterBiasSnapshot, key: RouterBiasCellKey): RouterBiasSteps {\n\tconst wantKey = cellKeyString(key);\n\tfor (const cell of snapshot.biasCells) {\n\t\tif (cellKeyString(cell) === wantKey) return cell.biasSteps;\n\t}\n\treturn 0;\n}\n\n// ============================================================================\n// Snapshot parser / loader (Goal 010 Lane I)\n// ============================================================================\n//\n// Everything below lets a caller safely turn untrusted on-disk JSON text into\n// a validated RouterBiasSnapshot. Same posture as isRouterFeedbackRecord in\n// router-feedback-collector.ts: an exact positive key-set allowlist (not a\n// denylist), bounded per-field checks reusing the ledger's own closed enums,\n// and \"never throw, return null/false instead\" on any malformed input.\n\n/** Bounded bias-step values a compiled cell may carry; mirrors RouterBiasSteps. */\nconst ROUTER_BIAS_STEPS_VALUES: readonly RouterBiasSteps[] = [-2, -1, 0, 1, 2];\n\n/** Exact allowed key set for one compiled `RouterBiasCell`, sorted for canonical set-equality comparison. */\nconst ROUTER_BIAS_CELL_KEYS: readonly string[] = [\n\t\"predictedClass\",\n\t\"laneType\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n\t\"biasSteps\",\n\t\"nStrong\",\n\t\"nTotal\",\n].sort();\n\n/** Exact allowed key set for a `RouterBiasSnapshot`, sorted for canonical set-equality comparison. */\nconst ROUTER_BIAS_SNAPSHOT_KEYS: readonly string[] = [\n\t\"schemaVersion\",\n\t\"sourceRecordDigest\",\n\t\"consideredCount\",\n\t\"droppedCount\",\n\t\"biasCells\",\n].sort();\n\n/** sha256 hex digest shape check for `sourceRecordDigest` (never trusts the value is actually a real digest of anything, only that it has the right shape). */\nconst SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactKeySet(value: Record<string, unknown>, allowedSortedKeys: readonly string[]): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== allowedSortedKeys.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== allowedSortedKeys[i]) return false;\n\t}\n\treturn true;\n}\n\nfunction isBoundedNonNegativeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/**\n * Strict runtime validator for one compiled bias cell. Exact eight-key\n * allowlist plus bounded per-field checks, reusing the same closed enums as\n * `isRouterFeedbackRecord` (`ROUTER_FEEDBACK_TASK_CLASSES`,\n * `ROUTER_FEEDBACK_LANE_TYPES`, `ROUTER_FEEDBACK_LEN_BUCKETS`) so this\n * validator cannot silently drift from the ledger's own schema. `nStrong` is\n * additionally required to never exceed `nTotal` (true by construction for\n * every cell `compileBiasSnapshot` can produce; a cheap sanity bound against\n * a hand-tampered file).\n */\nexport function isRouterBiasCell(value: unknown): value is RouterBiasCell {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactKeySet(value, ROUTER_BIAS_CELL_KEYS)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\tif (!ROUTER_BIAS_STEPS_VALUES.includes(value.biasSteps as RouterBiasSteps)) return false;\n\tif (!isBoundedNonNegativeInteger(value.nStrong)) return false;\n\tif (!isBoundedNonNegativeInteger(value.nTotal)) return false;\n\tif ((value.nStrong as number) > (value.nTotal as number)) return false;\n\treturn true;\n}\n\n/**\n * Strict runtime validator for a compiled bias snapshot. Exact five-key\n * allowlist; `biasCells` must be an array whose every element passes\n * `isRouterBiasCell`. Never trusts on-disk content: any unexpected shape,\n * extra key, or out-of-range value returns `false` rather than throwing.\n */\nexport function isRouterBiasSnapshot(value: unknown): value is RouterBiasSnapshot {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactKeySet(value, ROUTER_BIAS_SNAPSHOT_KEYS)) return false;\n\tif (value.schemaVersion !== \"router-bias-snapshot/v1\") return false;\n\tif (typeof value.sourceRecordDigest !== \"string\" || !SHA256_HEX_PATTERN.test(value.sourceRecordDigest)) return false;\n\tif (!isBoundedNonNegativeInteger(value.consideredCount)) return false;\n\tif (!isBoundedNonNegativeInteger(value.droppedCount)) return false;\n\tif (!Array.isArray(value.biasCells)) return false;\n\tfor (const cell of value.biasCells) {\n\t\tif (!isRouterBiasCell(cell)) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Safe parse of raw (untrusted, e.g. on-disk) JSON text into a validated\n * `RouterBiasSnapshot`. Never throws: malformed JSON or any schema violation\n * returns `null`. Performs no filesystem access itself — callers own the\n * actual read — so this module's zero-I/O determinism guarantee for\n * `compileBiasSnapshot`/`getBiasStepsForCell` is unaffected.\n */\nexport function parseRouterBiasSnapshot(raw: string): RouterBiasSnapshot | null {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\treturn isRouterBiasSnapshot(parsed) ? parsed : null;\n}\n\n/**\n * Default compiled-bias-snapshot path:\n * `<agentDir>/router-feedback/weights/router-bias-snapshot.<version>.json` —\n * matches `compile-bias-snapshot.ts`'s own default `--out` naming exactly, so\n * a snapshot compiled offline for `version` is found here with no extra\n * configuration. Owner-only agent dir, never repo-local (mirrors\n * `getDefaultRouterFeedbackLedgerPath`). Defaults to \"v4\", the router version\n * this lane wires bias *consumption* for.\n */\nexport function getDefaultRouterBiasSnapshotPath(version: RouterFeedbackVersion = \"v4\"): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"weights\", `router-bias-snapshot.${version}.json`);\n}\n"]}
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Reasoning-router bias compiler — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).
3
+ *
4
+ * Pure, deterministic compiler from a set of privacy-safe feedback records
5
+ * (see router-feedback-collector.ts) to a bounded, per-cell reasoning-level
6
+ * bias snapshot. No clock, randomness, network, or model calls: the same
7
+ * input array always produces the same output (`JSON.stringify` byte-
8
+ * identical), regardless of input record order. This module is a standalone,
9
+ * pure utility — nothing here is wired into a router resolver or
10
+ * agent-session.ts by this lane.
11
+ *
12
+ * Goal 010 Lane I addition: `isRouterBiasCell` / `isRouterBiasSnapshot` /
13
+ * `parseRouterBiasSnapshot` / `getDefaultRouterBiasSnapshotPath` below let a
14
+ * (now opt-in) caller safely load a snapshot this module previously only
15
+ * wrote. `parseRouterBiasSnapshot` performs no filesystem access itself — it
16
+ * only validates already-read text — so this module's zero-I/O determinism
17
+ * guarantee for `compileBiasSnapshot`/`getBiasStepsForCell` is unchanged; the
18
+ * actual disk read lives in agent-session.ts.
19
+ */
20
+ import { createHash } from "crypto";
21
+ import { join } from "path";
22
+ import { getAgentDir } from "../config.js";
23
+ import { isRouterFeedbackRecord, ROUTER_FEEDBACK_LANE_TYPES, ROUTER_FEEDBACK_LEN_BUCKETS, ROUTER_FEEDBACK_TASK_CLASSES, } from "./router-feedback-collector.js";
24
+ /** Minimum non-neutral ("strong") record count per cell before any bias applies. */
25
+ export const BIAS_STRONG_THRESHOLD = 5;
26
+ /** Bias magnitude bound, matching the resolver's existing bias clamp (reasoning-router-v2.ts BIAS_MAX). */
27
+ export const BIAS_MAX_STEPS = 2;
28
+ function cellKeyString(key) {
29
+ return [key.predictedClass, key.laneType, String(key.lenBucket), String(key.hadFence), String(key.hadDiff)].join("|");
30
+ }
31
+ /** +1 "needed more effort", -1 "needed less effort", 0 neutral (no directional bias contribution). */
32
+ function outcomeDirection(outcome) {
33
+ if (outcome === "up" || outcome === "fail" || outcome === "debug-follow-up")
34
+ return 1;
35
+ if (outcome === "down")
36
+ return -1;
37
+ return 0;
38
+ }
39
+ /** Fixed-key-order JSON so identical records always canonicalize to identical bytes. */
40
+ function canonicalRecordJson(record) {
41
+ return JSON.stringify({
42
+ routerVersion: record.routerVersion,
43
+ laneType: record.laneType,
44
+ predictedClass: record.predictedClass,
45
+ resolvedLevel: record.resolvedLevel,
46
+ acceptedLevel: record.acceptedLevel,
47
+ signal: record.signal,
48
+ outcome: record.outcome,
49
+ lenBucket: record.lenBucket,
50
+ hadFence: record.hadFence,
51
+ hadDiff: record.hadDiff,
52
+ });
53
+ }
54
+ function computeBiasSteps(upVotes, downVotes, nStrong) {
55
+ if (nStrong < BIAS_STRONG_THRESHOLD)
56
+ return 0;
57
+ const majority = upVotes - downVotes;
58
+ if (majority === 0)
59
+ return 0;
60
+ const dominance = Math.abs(majority) / nStrong;
61
+ const magnitude = dominance >= 0.75 ? BIAS_MAX_STEPS : 1;
62
+ const signedMagnitude = majority > 0 ? magnitude : -magnitude;
63
+ return Math.max(-BIAS_MAX_STEPS, Math.min(BIAS_MAX_STEPS, signedMagnitude));
64
+ }
65
+ /**
66
+ * Deterministically compile a bounded bias snapshot from raw (possibly
67
+ * malformed) ledger entries. Unknown-shaped entries — including ones with
68
+ * extra keys, missing keys, or out-of-range enum values — are dropped and
69
+ * counted, never thrown. Records tagged with a `routerVersion` other than the
70
+ * target version are also dropped and counted (never merged across router
71
+ * versions into one snapshot). An empty or all-dropped input compiles to an
72
+ * empty-bias snapshot (`biasCells: []`), which is exactly the shipped
73
+ * no-learning behavior — this makes "empty ledger -> zero bias" true by
74
+ * construction, not by a special case.
75
+ *
76
+ * Pure: same `records` (any order) -> same `RouterBiasSnapshot` (byte-
77
+ * identical `JSON.stringify` output), because cells are grouped by an
78
+ * order-independent key and the emitted cell/record ordering is always
79
+ * sorted before serialization.
80
+ */
81
+ export function compileBiasSnapshot(records, options = {}) {
82
+ const routerVersion = options.routerVersion ?? "v3";
83
+ const valid = [];
84
+ let droppedCount = 0;
85
+ for (const entry of records) {
86
+ if (isRouterFeedbackRecord(entry) && entry.routerVersion === routerVersion) {
87
+ valid.push(entry);
88
+ }
89
+ else {
90
+ droppedCount += 1;
91
+ }
92
+ }
93
+ const cells = new Map();
94
+ for (const record of valid) {
95
+ const key = {
96
+ predictedClass: record.predictedClass,
97
+ laneType: record.laneType,
98
+ lenBucket: record.lenBucket,
99
+ hadFence: record.hadFence,
100
+ hadDiff: record.hadDiff,
101
+ };
102
+ const mapKey = cellKeyString(key);
103
+ const existing = cells.get(mapKey) ?? { ...key, nTotal: 0, upVotes: 0, downVotes: 0 };
104
+ existing.nTotal += 1;
105
+ const direction = outcomeDirection(record.outcome);
106
+ if (direction === 1)
107
+ existing.upVotes += 1;
108
+ else if (direction === -1)
109
+ existing.downVotes += 1;
110
+ cells.set(mapKey, existing);
111
+ }
112
+ const biasCells = [];
113
+ for (const mapKey of [...cells.keys()].sort()) {
114
+ const acc = cells.get(mapKey);
115
+ if (!acc)
116
+ continue;
117
+ const nStrong = acc.upVotes + acc.downVotes;
118
+ biasCells.push({
119
+ predictedClass: acc.predictedClass,
120
+ laneType: acc.laneType,
121
+ lenBucket: acc.lenBucket,
122
+ hadFence: acc.hadFence,
123
+ hadDiff: acc.hadDiff,
124
+ biasSteps: computeBiasSteps(acc.upVotes, acc.downVotes, nStrong),
125
+ nStrong,
126
+ nTotal: acc.nTotal,
127
+ });
128
+ }
129
+ const canonicalValid = valid.map(canonicalRecordJson).sort();
130
+ const sourceRecordDigest = createHash("sha256").update(canonicalValid.join("\n")).digest("hex");
131
+ return {
132
+ schemaVersion: "router-bias-snapshot/v1",
133
+ sourceRecordDigest,
134
+ consideredCount: valid.length,
135
+ droppedCount,
136
+ biasCells,
137
+ };
138
+ }
139
+ /** Look up the compiled bias for one feature cell; 0 (no bias) when the cell is absent from the snapshot. */
140
+ export function getBiasStepsForCell(snapshot, key) {
141
+ const wantKey = cellKeyString(key);
142
+ for (const cell of snapshot.biasCells) {
143
+ if (cellKeyString(cell) === wantKey)
144
+ return cell.biasSteps;
145
+ }
146
+ return 0;
147
+ }
148
+ // ============================================================================
149
+ // Snapshot parser / loader (Goal 010 Lane I)
150
+ // ============================================================================
151
+ //
152
+ // Everything below lets a caller safely turn untrusted on-disk JSON text into
153
+ // a validated RouterBiasSnapshot. Same posture as isRouterFeedbackRecord in
154
+ // router-feedback-collector.ts: an exact positive key-set allowlist (not a
155
+ // denylist), bounded per-field checks reusing the ledger's own closed enums,
156
+ // and "never throw, return null/false instead" on any malformed input.
157
+ /** Bounded bias-step values a compiled cell may carry; mirrors RouterBiasSteps. */
158
+ const ROUTER_BIAS_STEPS_VALUES = [-2, -1, 0, 1, 2];
159
+ /** Exact allowed key set for one compiled `RouterBiasCell`, sorted for canonical set-equality comparison. */
160
+ const ROUTER_BIAS_CELL_KEYS = [
161
+ "predictedClass",
162
+ "laneType",
163
+ "lenBucket",
164
+ "hadFence",
165
+ "hadDiff",
166
+ "biasSteps",
167
+ "nStrong",
168
+ "nTotal",
169
+ ].sort();
170
+ /** Exact allowed key set for a `RouterBiasSnapshot`, sorted for canonical set-equality comparison. */
171
+ const ROUTER_BIAS_SNAPSHOT_KEYS = [
172
+ "schemaVersion",
173
+ "sourceRecordDigest",
174
+ "consideredCount",
175
+ "droppedCount",
176
+ "biasCells",
177
+ ].sort();
178
+ /** sha256 hex digest shape check for `sourceRecordDigest` (never trusts the value is actually a real digest of anything, only that it has the right shape). */
179
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
180
+ function isPlainObject(value) {
181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
182
+ }
183
+ function hasExactKeySet(value, allowedSortedKeys) {
184
+ const keys = Object.keys(value).sort();
185
+ if (keys.length !== allowedSortedKeys.length)
186
+ return false;
187
+ for (let i = 0; i < keys.length; i++) {
188
+ if (keys[i] !== allowedSortedKeys[i])
189
+ return false;
190
+ }
191
+ return true;
192
+ }
193
+ function isBoundedNonNegativeInteger(value) {
194
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
195
+ }
196
+ /**
197
+ * Strict runtime validator for one compiled bias cell. Exact eight-key
198
+ * allowlist plus bounded per-field checks, reusing the same closed enums as
199
+ * `isRouterFeedbackRecord` (`ROUTER_FEEDBACK_TASK_CLASSES`,
200
+ * `ROUTER_FEEDBACK_LANE_TYPES`, `ROUTER_FEEDBACK_LEN_BUCKETS`) so this
201
+ * validator cannot silently drift from the ledger's own schema. `nStrong` is
202
+ * additionally required to never exceed `nTotal` (true by construction for
203
+ * every cell `compileBiasSnapshot` can produce; a cheap sanity bound against
204
+ * a hand-tampered file).
205
+ */
206
+ export function isRouterBiasCell(value) {
207
+ if (!isPlainObject(value))
208
+ return false;
209
+ if (!hasExactKeySet(value, ROUTER_BIAS_CELL_KEYS))
210
+ return false;
211
+ if (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass))
212
+ return false;
213
+ if (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType))
214
+ return false;
215
+ if (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket))
216
+ return false;
217
+ if (typeof value.hadFence !== "boolean")
218
+ return false;
219
+ if (typeof value.hadDiff !== "boolean")
220
+ return false;
221
+ if (!ROUTER_BIAS_STEPS_VALUES.includes(value.biasSteps))
222
+ return false;
223
+ if (!isBoundedNonNegativeInteger(value.nStrong))
224
+ return false;
225
+ if (!isBoundedNonNegativeInteger(value.nTotal))
226
+ return false;
227
+ if (value.nStrong > value.nTotal)
228
+ return false;
229
+ return true;
230
+ }
231
+ /**
232
+ * Strict runtime validator for a compiled bias snapshot. Exact five-key
233
+ * allowlist; `biasCells` must be an array whose every element passes
234
+ * `isRouterBiasCell`. Never trusts on-disk content: any unexpected shape,
235
+ * extra key, or out-of-range value returns `false` rather than throwing.
236
+ */
237
+ export function isRouterBiasSnapshot(value) {
238
+ if (!isPlainObject(value))
239
+ return false;
240
+ if (!hasExactKeySet(value, ROUTER_BIAS_SNAPSHOT_KEYS))
241
+ return false;
242
+ if (value.schemaVersion !== "router-bias-snapshot/v1")
243
+ return false;
244
+ if (typeof value.sourceRecordDigest !== "string" || !SHA256_HEX_PATTERN.test(value.sourceRecordDigest))
245
+ return false;
246
+ if (!isBoundedNonNegativeInteger(value.consideredCount))
247
+ return false;
248
+ if (!isBoundedNonNegativeInteger(value.droppedCount))
249
+ return false;
250
+ if (!Array.isArray(value.biasCells))
251
+ return false;
252
+ for (const cell of value.biasCells) {
253
+ if (!isRouterBiasCell(cell))
254
+ return false;
255
+ }
256
+ return true;
257
+ }
258
+ /**
259
+ * Safe parse of raw (untrusted, e.g. on-disk) JSON text into a validated
260
+ * `RouterBiasSnapshot`. Never throws: malformed JSON or any schema violation
261
+ * returns `null`. Performs no filesystem access itself — callers own the
262
+ * actual read — so this module's zero-I/O determinism guarantee for
263
+ * `compileBiasSnapshot`/`getBiasStepsForCell` is unaffected.
264
+ */
265
+ export function parseRouterBiasSnapshot(raw) {
266
+ let parsed;
267
+ try {
268
+ parsed = JSON.parse(raw);
269
+ }
270
+ catch {
271
+ return null;
272
+ }
273
+ return isRouterBiasSnapshot(parsed) ? parsed : null;
274
+ }
275
+ /**
276
+ * Default compiled-bias-snapshot path:
277
+ * `<agentDir>/router-feedback/weights/router-bias-snapshot.<version>.json` —
278
+ * matches `compile-bias-snapshot.ts`'s own default `--out` naming exactly, so
279
+ * a snapshot compiled offline for `version` is found here with no extra
280
+ * configuration. Owner-only agent dir, never repo-local (mirrors
281
+ * `getDefaultRouterFeedbackLedgerPath`). Defaults to "v4", the router version
282
+ * this lane wires bias *consumption* for.
283
+ */
284
+ export function getDefaultRouterBiasSnapshotPath(version = "v4") {
285
+ return join(getAgentDir(), "router-feedback", "weights", `router-bias-snapshot.${version}.json`);
286
+ }
287
+ //# sourceMappingURL=reasoning-router-bias.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-router-bias.js","sourceRoot":"","sources":["../../src/core/reasoning-router-bias.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reasoning-router bias compiler — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).\n *\n * Pure, deterministic compiler from a set of privacy-safe feedback records\n * (see router-feedback-collector.ts) to a bounded, per-cell reasoning-level\n * bias snapshot. No clock, randomness, network, or model calls: the same\n * input array always produces the same output (`JSON.stringify` byte-\n * identical), regardless of input record order. This module is a standalone,\n * pure utility — nothing here is wired into a router resolver or\n * agent-session.ts by this lane.\n *\n * Goal 010 Lane I addition: `isRouterBiasCell` / `isRouterBiasSnapshot` /\n * `parseRouterBiasSnapshot` / `getDefaultRouterBiasSnapshotPath` below let a\n * (now opt-in) caller safely load a snapshot this module previously only\n * wrote. `parseRouterBiasSnapshot` performs no filesystem access itself — it\n * only validates already-read text — so this module's zero-I/O determinism\n * guarantee for `compileBiasSnapshot`/`getBiasStepsForCell` is unchanged; the\n * actual disk read lives in agent-session.ts.\n */\n\nimport { createHash } from \"crypto\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tisRouterFeedbackRecord,\n\tROUTER_FEEDBACK_LANE_TYPES,\n\tROUTER_FEEDBACK_LEN_BUCKETS,\n\tROUTER_FEEDBACK_TASK_CLASSES,\n\ttype RouterFeedbackLaneType,\n\ttype RouterFeedbackLenBucket,\n\ttype RouterFeedbackRecord,\n\ttype RouterFeedbackTaskClass,\n\ttype RouterFeedbackVersion,\n} from \"./router-feedback-collector.ts\";\n\n/** Minimum non-neutral (\"strong\") record count per cell before any bias applies. */\nexport const BIAS_STRONG_THRESHOLD = 5;\n\n/** Bias magnitude bound, matching the resolver's existing bias clamp (reasoning-router-v2.ts BIAS_MAX). */\nexport const BIAS_MAX_STEPS = 2;\n\n/** Bounded per-cell bias, in ladder steps. Never outside [-2, 2]. */\nexport type RouterBiasSteps = -2 | -1 | 0 | 1 | 2;\n\n/** The feature tuple a bias cell is keyed on — identical dimensions the router already computes locally. */\nexport interface RouterBiasCellKey {\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport interface RouterBiasCell extends RouterBiasCellKey {\n\treadonly biasSteps: RouterBiasSteps;\n\t/** Non-neutral (up/down-signaling) record count for this cell. */\n\treadonly nStrong: number;\n\t/** Total record count for this cell, including neutral (accepted/same/pass) records. */\n\treadonly nTotal: number;\n}\n\nexport interface RouterBiasSnapshot {\n\treadonly schemaVersion: \"router-bias-snapshot/v1\";\n\t/** sha256 hex digest of the canonicalized valid-record set actually compiled; not a prompt hash. */\n\treadonly sourceRecordDigest: string;\n\t/** Count of records that were validated and included in this snapshot. */\n\treadonly consideredCount: number;\n\t/** Count of records excluded from this snapshot (schema-invalid, or router-version mismatch). */\n\treadonly droppedCount: number;\n\t/** Bias cells, sorted deterministically. Empty when there is no ledger data or no cell reaches the threshold. */\n\treadonly biasCells: readonly RouterBiasCell[];\n}\n\nexport interface CompileBiasSnapshotOptions {\n\t/**\n\t * Restrict compilation to one router version — `compileBiasSnapshot` never\n\t * mixes records from different router versions into one snapshot. Records\n\t * tagged with any other `routerVersion` are excluded from this snapshot and\n\t * counted in `droppedCount`. Defaults to \"v3\" for API compatibility; the\n\t * offline CLI now defaults to \"v4\", matching the only auto-router wired to\n\t * consume a learning-bias snapshot at runtime.\n\t */\n\treadonly routerVersion?: RouterFeedbackVersion;\n}\n\nfunction cellKeyString(key: RouterBiasCellKey): string {\n\treturn [key.predictedClass, key.laneType, String(key.lenBucket), String(key.hadFence), String(key.hadDiff)].join(\n\t\t\"|\",\n\t);\n}\n\n/** +1 \"needed more effort\", -1 \"needed less effort\", 0 neutral (no directional bias contribution). */\nfunction outcomeDirection(outcome: RouterFeedbackRecord[\"outcome\"]): -1 | 0 | 1 {\n\tif (outcome === \"up\" || outcome === \"fail\" || outcome === \"debug-follow-up\") return 1;\n\tif (outcome === \"down\") return -1;\n\treturn 0;\n}\n\n/** Fixed-key-order JSON so identical records always canonicalize to identical bytes. */\nfunction canonicalRecordJson(record: RouterFeedbackRecord): string {\n\treturn JSON.stringify({\n\t\trouterVersion: record.routerVersion,\n\t\tlaneType: record.laneType,\n\t\tpredictedClass: record.predictedClass,\n\t\tresolvedLevel: record.resolvedLevel,\n\t\tacceptedLevel: record.acceptedLevel,\n\t\tsignal: record.signal,\n\t\toutcome: record.outcome,\n\t\tlenBucket: record.lenBucket,\n\t\thadFence: record.hadFence,\n\t\thadDiff: record.hadDiff,\n\t});\n}\n\ninterface CellAccumulator extends RouterBiasCellKey {\n\tnTotal: number;\n\tupVotes: number;\n\tdownVotes: number;\n}\n\nfunction computeBiasSteps(upVotes: number, downVotes: number, nStrong: number): RouterBiasSteps {\n\tif (nStrong < BIAS_STRONG_THRESHOLD) return 0;\n\tconst majority = upVotes - downVotes;\n\tif (majority === 0) return 0;\n\n\tconst dominance = Math.abs(majority) / nStrong;\n\tconst magnitude = dominance >= 0.75 ? BIAS_MAX_STEPS : 1;\n\tconst signedMagnitude = majority > 0 ? magnitude : -magnitude;\n\treturn Math.max(-BIAS_MAX_STEPS, Math.min(BIAS_MAX_STEPS, signedMagnitude)) as RouterBiasSteps;\n}\n\n/**\n * Deterministically compile a bounded bias snapshot from raw (possibly\n * malformed) ledger entries. Unknown-shaped entries — including ones with\n * extra keys, missing keys, or out-of-range enum values — are dropped and\n * counted, never thrown. Records tagged with a `routerVersion` other than the\n * target version are also dropped and counted (never merged across router\n * versions into one snapshot). An empty or all-dropped input compiles to an\n * empty-bias snapshot (`biasCells: []`), which is exactly the shipped\n * no-learning behavior — this makes \"empty ledger -> zero bias\" true by\n * construction, not by a special case.\n *\n * Pure: same `records` (any order) -> same `RouterBiasSnapshot` (byte-\n * identical `JSON.stringify` output), because cells are grouped by an\n * order-independent key and the emitted cell/record ordering is always\n * sorted before serialization.\n */\nexport function compileBiasSnapshot(\n\trecords: readonly unknown[],\n\toptions: CompileBiasSnapshotOptions = {},\n): RouterBiasSnapshot {\n\tconst routerVersion = options.routerVersion ?? \"v3\";\n\tconst valid: RouterFeedbackRecord[] = [];\n\tlet droppedCount = 0;\n\n\tfor (const entry of records) {\n\t\tif (isRouterFeedbackRecord(entry) && entry.routerVersion === routerVersion) {\n\t\t\tvalid.push(entry);\n\t\t} else {\n\t\t\tdroppedCount += 1;\n\t\t}\n\t}\n\n\tconst cells = new Map<string, CellAccumulator>();\n\tfor (const record of valid) {\n\t\tconst key: RouterBiasCellKey = {\n\t\t\tpredictedClass: record.predictedClass,\n\t\t\tlaneType: record.laneType,\n\t\t\tlenBucket: record.lenBucket,\n\t\t\thadFence: record.hadFence,\n\t\t\thadDiff: record.hadDiff,\n\t\t};\n\t\tconst mapKey = cellKeyString(key);\n\t\tconst existing = cells.get(mapKey) ?? { ...key, nTotal: 0, upVotes: 0, downVotes: 0 };\n\t\texisting.nTotal += 1;\n\t\tconst direction = outcomeDirection(record.outcome);\n\t\tif (direction === 1) existing.upVotes += 1;\n\t\telse if (direction === -1) existing.downVotes += 1;\n\t\tcells.set(mapKey, existing);\n\t}\n\n\tconst biasCells: RouterBiasCell[] = [];\n\tfor (const mapKey of [...cells.keys()].sort()) {\n\t\tconst acc = cells.get(mapKey);\n\t\tif (!acc) continue;\n\t\tconst nStrong = acc.upVotes + acc.downVotes;\n\t\tbiasCells.push({\n\t\t\tpredictedClass: acc.predictedClass,\n\t\t\tlaneType: acc.laneType,\n\t\t\tlenBucket: acc.lenBucket,\n\t\t\thadFence: acc.hadFence,\n\t\t\thadDiff: acc.hadDiff,\n\t\t\tbiasSteps: computeBiasSteps(acc.upVotes, acc.downVotes, nStrong),\n\t\t\tnStrong,\n\t\t\tnTotal: acc.nTotal,\n\t\t});\n\t}\n\n\tconst canonicalValid = valid.map(canonicalRecordJson).sort();\n\tconst sourceRecordDigest = createHash(\"sha256\").update(canonicalValid.join(\"\\n\")).digest(\"hex\");\n\n\treturn {\n\t\tschemaVersion: \"router-bias-snapshot/v1\",\n\t\tsourceRecordDigest,\n\t\tconsideredCount: valid.length,\n\t\tdroppedCount,\n\t\tbiasCells,\n\t};\n}\n\n/** Look up the compiled bias for one feature cell; 0 (no bias) when the cell is absent from the snapshot. */\nexport function getBiasStepsForCell(snapshot: RouterBiasSnapshot, key: RouterBiasCellKey): RouterBiasSteps {\n\tconst wantKey = cellKeyString(key);\n\tfor (const cell of snapshot.biasCells) {\n\t\tif (cellKeyString(cell) === wantKey) return cell.biasSteps;\n\t}\n\treturn 0;\n}\n\n// ============================================================================\n// Snapshot parser / loader (Goal 010 Lane I)\n// ============================================================================\n//\n// Everything below lets a caller safely turn untrusted on-disk JSON text into\n// a validated RouterBiasSnapshot. Same posture as isRouterFeedbackRecord in\n// router-feedback-collector.ts: an exact positive key-set allowlist (not a\n// denylist), bounded per-field checks reusing the ledger's own closed enums,\n// and \"never throw, return null/false instead\" on any malformed input.\n\n/** Bounded bias-step values a compiled cell may carry; mirrors RouterBiasSteps. */\nconst ROUTER_BIAS_STEPS_VALUES: readonly RouterBiasSteps[] = [-2, -1, 0, 1, 2];\n\n/** Exact allowed key set for one compiled `RouterBiasCell`, sorted for canonical set-equality comparison. */\nconst ROUTER_BIAS_CELL_KEYS: readonly string[] = [\n\t\"predictedClass\",\n\t\"laneType\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n\t\"biasSteps\",\n\t\"nStrong\",\n\t\"nTotal\",\n].sort();\n\n/** Exact allowed key set for a `RouterBiasSnapshot`, sorted for canonical set-equality comparison. */\nconst ROUTER_BIAS_SNAPSHOT_KEYS: readonly string[] = [\n\t\"schemaVersion\",\n\t\"sourceRecordDigest\",\n\t\"consideredCount\",\n\t\"droppedCount\",\n\t\"biasCells\",\n].sort();\n\n/** sha256 hex digest shape check for `sourceRecordDigest` (never trusts the value is actually a real digest of anything, only that it has the right shape). */\nconst SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactKeySet(value: Record<string, unknown>, allowedSortedKeys: readonly string[]): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== allowedSortedKeys.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== allowedSortedKeys[i]) return false;\n\t}\n\treturn true;\n}\n\nfunction isBoundedNonNegativeInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\n/**\n * Strict runtime validator for one compiled bias cell. Exact eight-key\n * allowlist plus bounded per-field checks, reusing the same closed enums as\n * `isRouterFeedbackRecord` (`ROUTER_FEEDBACK_TASK_CLASSES`,\n * `ROUTER_FEEDBACK_LANE_TYPES`, `ROUTER_FEEDBACK_LEN_BUCKETS`) so this\n * validator cannot silently drift from the ledger's own schema. `nStrong` is\n * additionally required to never exceed `nTotal` (true by construction for\n * every cell `compileBiasSnapshot` can produce; a cheap sanity bound against\n * a hand-tampered file).\n */\nexport function isRouterBiasCell(value: unknown): value is RouterBiasCell {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactKeySet(value, ROUTER_BIAS_CELL_KEYS)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\tif (!ROUTER_BIAS_STEPS_VALUES.includes(value.biasSteps as RouterBiasSteps)) return false;\n\tif (!isBoundedNonNegativeInteger(value.nStrong)) return false;\n\tif (!isBoundedNonNegativeInteger(value.nTotal)) return false;\n\tif ((value.nStrong as number) > (value.nTotal as number)) return false;\n\treturn true;\n}\n\n/**\n * Strict runtime validator for a compiled bias snapshot. Exact five-key\n * allowlist; `biasCells` must be an array whose every element passes\n * `isRouterBiasCell`. Never trusts on-disk content: any unexpected shape,\n * extra key, or out-of-range value returns `false` rather than throwing.\n */\nexport function isRouterBiasSnapshot(value: unknown): value is RouterBiasSnapshot {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactKeySet(value, ROUTER_BIAS_SNAPSHOT_KEYS)) return false;\n\tif (value.schemaVersion !== \"router-bias-snapshot/v1\") return false;\n\tif (typeof value.sourceRecordDigest !== \"string\" || !SHA256_HEX_PATTERN.test(value.sourceRecordDigest)) return false;\n\tif (!isBoundedNonNegativeInteger(value.consideredCount)) return false;\n\tif (!isBoundedNonNegativeInteger(value.droppedCount)) return false;\n\tif (!Array.isArray(value.biasCells)) return false;\n\tfor (const cell of value.biasCells) {\n\t\tif (!isRouterBiasCell(cell)) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Safe parse of raw (untrusted, e.g. on-disk) JSON text into a validated\n * `RouterBiasSnapshot`. Never throws: malformed JSON or any schema violation\n * returns `null`. Performs no filesystem access itself — callers own the\n * actual read — so this module's zero-I/O determinism guarantee for\n * `compileBiasSnapshot`/`getBiasStepsForCell` is unaffected.\n */\nexport function parseRouterBiasSnapshot(raw: string): RouterBiasSnapshot | null {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\treturn isRouterBiasSnapshot(parsed) ? parsed : null;\n}\n\n/**\n * Default compiled-bias-snapshot path:\n * `<agentDir>/router-feedback/weights/router-bias-snapshot.<version>.json` —\n * matches `compile-bias-snapshot.ts`'s own default `--out` naming exactly, so\n * a snapshot compiled offline for `version` is found here with no extra\n * configuration. Owner-only agent dir, never repo-local (mirrors\n * `getDefaultRouterFeedbackLedgerPath`). Defaults to \"v4\", the router version\n * this lane wires bias *consumption* for.\n */\nexport function getDefaultRouterBiasSnapshotPath(version: RouterFeedbackVersion = \"v4\"): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"weights\", `router-bias-snapshot.${version}.json`);\n}\n"]}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Reasoning-router v2 — pure weighted classifier + resolver (Goal 004, Lane I2/I3).
3
+ *
4
+ * Deterministic by construction: same (input, weights) -> same TaskClassV2, and
5
+ * same (taskClass, availableLevels, laneType, bias, hint) -> same ThinkingLevel.
6
+ * No clock, randomness, or I/O. Erasable TypeScript only (ThinkingLevel is
7
+ * imported type-only and erased).
8
+ *
9
+ * ============================================================================
10
+ * v1-COMPAT BEHAVIOR UNDER DEFAULT_WEIGHTS
11
+ * ============================================================================
12
+ * With DEFAULT_WEIGHTS (frozen in reasoning-router-weights.ts), classifyTaskV2
13
+ * reproduces v1 classifyTask on the full v1 corpus (Goal 004 Req 1.4), with one
14
+ * INTENTIONAL fix family: a lone WEAK-debug trigger ("fix") no longer dominates
15
+ * a stronger class signal. Concretely, "fix the typo" classifies as simple-edit,
16
+ * not debug (Goal 004 Req 1.3 — the v1 regression case). This is achieved by the
17
+ * STRONG/WEAK keyword split below: "fix" lives in WEAK-debug only, and
18
+ * DEFAULT_WEIGHTS weights only the STRONG diagonal, so a STRONG simple-edit hit
19
+ * ("typo") outscores the unweighted WEAK "fix".
20
+ *
21
+ * The zero-score cascade (v1 steps 2-6) runs verbatim whenever every scoreClass
22
+ * term is 0, preserving v1's length/lane/default behavior for signal-free turns.
23
+ * sticky/consult/margin thresholds are 0 in DEFAULT_WEIGHTS and treated as
24
+ * INACTIVE (<= 0), so v1-compat never enters the sticky/consult/judge paths.
25
+ * ============================================================================
26
+ */
27
+ import type { ThinkingLevel } from "omk-agent-core";
28
+ import { type ReasoningLaneTypeV2, type RouterWeights, type TaskClassV2 } from "./reasoning-router-weights.ts";
29
+ export type { ReasoningLaneTypeV2, RouterFeatures, RouterWeights, TaskClassV2, } from "./reasoning-router-weights.ts";
30
+ export { DEFAULT_WEIGHTS, scoreClass, TASK_CLASSES_V2 } from "./reasoning-router-weights.ts";
31
+ /** Ring-buffer cap for multi-turn history (caller-owned). */
32
+ declare const HISTORY_CAP = 8;
33
+ /**
34
+ * Caller-supplied turn input.
35
+ *
36
+ * - prompt: raw turn text (trimmed by the classifier).
37
+ * - laneType: subagent lane, if any (drives lane fallback + resolver step).
38
+ * - history: N=8 ring buffer of prior task classes, NEWEST FIRST (multi-turn).
39
+ * - pressureBucket: context-pressure band 0..3 (see RouterFeatures).
40
+ * - judgeVote: optional tier-2 LLM-judge label (default OFF; injected as data).
41
+ */
42
+ export interface TaskClassifierInputV2 {
43
+ prompt: string;
44
+ laneType?: ReasoningLaneTypeV2;
45
+ history?: readonly TaskClassV2[];
46
+ pressureBucket?: number;
47
+ judgeVote?: TaskClassV2 | null;
48
+ }
49
+ /**
50
+ * Deterministic weighted multi-signal classifier (Goal 004 Req 1).
51
+ *
52
+ * Pipeline:
53
+ * 1. Extract RouterFeatures from input (STRONG/WEAK keyword 0/1 counts per
54
+ * family, codeFence, diffHunk, lenBucket, multiTurnPrior, pressureBucket,
55
+ * judgeVote).
56
+ * 2. Score every class via the frozen scoreClass(c, f, weights). top1 = argmax
57
+ * with v1-precedence tie-break; margin = top1 - top2.
58
+ * 3. ZERO-SCORE CASCADE (v1-compat): when maxScore === 0, fall through exactly
59
+ * like v1 steps 2-6 (fence/diff -> code-gen; len<40 -> trivial; len>=2400
60
+ * -> plan; lane fallback; default code-gen).
61
+ * 4. HYSTERESIS: only when weights.stickyThreshold > 0 (INACTIVE for
62
+ * DEFAULT_WEIGHTS). If margin < stickyThreshold and history is non-empty,
63
+ * keep the previous class unless a STRONG keyword matched for top1.
64
+ *
65
+ * Pure: same (input, weights) -> same TaskClassV2.
66
+ */
67
+ export declare function classifyTaskV2(input: TaskClassifierInputV2, weights: RouterWeights): TaskClassV2;
68
+ /**
69
+ * Static rule table: task class -> recommended ThinkingLevel (before lane
70
+ * adjustment, bias, hint, and clamping). Identical to v1 TASK_CLASS_THINKING_LEVELS.
71
+ */
72
+ export declare const TASK_CLASS_THINKING_LEVELS_V2: Readonly<Record<TaskClassV2, ThinkingLevel>>;
73
+ /**
74
+ * Resolve a ThinkingLevel for a task class (Goal 004 resolver).
75
+ *
76
+ * Pipeline: base rule table -> lane step -> bounded bias [-2,+2] -> optional hint
77
+ * fusion (±2 bounded) -> clamp to availableLevels (v1 algorithm).
78
+ *
79
+ * The clamp guarantees model-capability safety: the return value is always a
80
+ * member of availableLevels and never "off" for reasoning models. With
81
+ * bias=0 and hint=null, output is identical to v1 resolveThinkingLevel.
82
+ */
83
+ export declare function resolveThinkingLevelV2(taskClass: TaskClassV2, availableLevels: readonly ThinkingLevel[], laneType: ReasoningLaneTypeV2 | undefined, bias: number, hint: {
84
+ level: ThinkingLevel;
85
+ confidence: number;
86
+ } | null): ThinkingLevel;
87
+ /**
88
+ * Thin auto-mode wrapper: resolve with bias=0 and hint=null (what the session
89
+ * calls in v2 auto thinking mode). Output equals v1 resolveThinkingLevel for the
90
+ * same (taskClass, availableLevels, laneType).
91
+ */
92
+ export declare function resolveThinkingLevelV2ForAuto(taskClass: TaskClassV2, availableLevels: readonly ThinkingLevel[], laneType: ReasoningLaneTypeV2 | undefined): ThinkingLevel;
93
+ export { HISTORY_CAP };
94
+ //# sourceMappingURL=reasoning-router-v2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-router-v2.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,EACN,KAAK,mBAAmB,EAExB,KAAK,aAAa,EAGlB,KAAK,WAAW,EAChB,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACX,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,WAAW,GACX,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAM7F,6DAA6D;AAC7D,QAAA,MAAM,WAAW,IAAI,CAAC;AAwGtB;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,aAAa,GAAG,WAAW,CAsEhG;AAKD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAQtF,CAAC;AAiBF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACrC,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GACvD,aAAa,CA4Bf;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GACvC,aAAa,CAEf;AAED,OAAO,EAAE,WAAW,EAAE,CAAC","sourcesContent":["/**\n * Reasoning-router v2 — pure weighted classifier + resolver (Goal 004, Lane I2/I3).\n *\n * Deterministic by construction: same (input, weights) -> same TaskClassV2, and\n * same (taskClass, availableLevels, laneType, bias, hint) -> same ThinkingLevel.\n * No clock, randomness, or I/O. Erasable TypeScript only (ThinkingLevel is\n * imported type-only and erased).\n *\n * ============================================================================\n * v1-COMPAT BEHAVIOR UNDER DEFAULT_WEIGHTS\n * ============================================================================\n * With DEFAULT_WEIGHTS (frozen in reasoning-router-weights.ts), classifyTaskV2\n * reproduces v1 classifyTask on the full v1 corpus (Goal 004 Req 1.4), with one\n * INTENTIONAL fix family: a lone WEAK-debug trigger (\"fix\") no longer dominates\n * a stronger class signal. Concretely, \"fix the typo\" classifies as simple-edit,\n * not debug (Goal 004 Req 1.3 — the v1 regression case). This is achieved by the\n * STRONG/WEAK keyword split below: \"fix\" lives in WEAK-debug only, and\n * DEFAULT_WEIGHTS weights only the STRONG diagonal, so a STRONG simple-edit hit\n * (\"typo\") outscores the unweighted WEAK \"fix\".\n *\n * The zero-score cascade (v1 steps 2-6) runs verbatim whenever every scoreClass\n * term is 0, preserving v1's length/lane/default behavior for signal-free turns.\n * sticky/consult/margin thresholds are 0 in DEFAULT_WEIGHTS and treated as\n * INACTIVE (<= 0), so v1-compat never enters the sticky/consult/judge paths.\n * ============================================================================\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\n\nimport {\n\ttype ReasoningLaneTypeV2,\n\ttype RouterFeatures,\n\ttype RouterWeights,\n\tscoreClass,\n\tTASK_CLASSES_V2,\n\ttype TaskClassV2,\n} from \"./reasoning-router-weights.ts\";\n\nexport type {\n\tReasoningLaneTypeV2,\n\tRouterFeatures,\n\tRouterWeights,\n\tTaskClassV2,\n} from \"./reasoning-router-weights.ts\";\nexport { DEFAULT_WEIGHTS, scoreClass, TASK_CLASSES_V2 } from \"./reasoning-router-weights.ts\";\n\n/** Prompts shorter than this (trimmed) classify as trivial via the zero-score cascade. */\nconst TRIVIAL_MAX_CHARS = 40;\n/** Prompts at/above this length with no signal are treated as long prose briefs (plan). */\nconst COMPLEX_PROSE_MIN_CHARS = 2400;\n/** Ring-buffer cap for multi-turn history (caller-owned). */\nconst HISTORY_CAP = 8;\n\n/**\n * STRONG (class-defining) keyword families, derived from v1 KEYWORD_FAMILIES\n * (reasoning-router.ts). Each pattern is the v1 alternation MINUS the ambiguous\n * WEAK tokens that are split out below. Default i (no `g` flag): stateless.\n */\nconst STRONG_KEYWORDS: ReadonlyArray<{ taskClass: TaskClassV2; pattern: RegExp }> = [\n\t{\n\t\ttaskClass: \"debug\",\n\t\tpattern:\n\t\t\t/\\b(debug(ging)?|bugs?|errors?|exceptions?|crash(es|ing|ed)?|stack\\s*trace|traceback|regressions?|broken|fail(s|ing|ed|ure|ures)?|flaky|reproduce)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"refactor\",\n\t\tpattern:\n\t\t\t/\\b(refactor(ing|ed)?|restructur(e|ing)|renam(e|ing)|extract(ing)?|clean\\s*up|cleanup|deduplicat(e|ing)|reorganiz(e|ing)|modulariz(e|ing))\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"review\",\n\t\tpattern: /\\b(review(s|ing|ed)?|audit(s|ing|ed)?|critique|inspect(ing)?)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"plan\",\n\t\tpattern:\n\t\t\t/\\b(plan(s|ning)?|design(s|ing)?|architect(s|ure|ing)?|roadmap|spec(s|ification|ifications)?|strateg(y|ies|ize)|decompos(e|ing|ition)|milestones?)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"simple-edit\",\n\t\tpattern:\n\t\t\t/\\b(typos?|tweak(s|ing|ed)?|one-?liner?|single\\s+line|bump(s|ing|ed)?|whitespace|reword(s|ing|ed)?|indentation|punctuation)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"code-gen\",\n\t\tpattern:\n\t\t\t/\\b(implement(s|ing|ation)?|writ(e|ing)|creat(e|es|ing)|build(s|ing)?|generat(e|es|ing)|scaffold(ing)?|prototype)\\b/i,\n\t},\n];\n\n/**\n * WEAK (ambiguous) keyword sub-patterns split out of the v1 alternations. Under\n * DEFAULT_WEIGHTS these are INERT (the weight matrix has no WEAK coefficients),\n * so they do not affect v1-compat; they exist for calibrated weight presets.\n * The critical entry is debug \"fix\": a lone \"fix\" without a STRONG debug\n * co-trigger no longer wins debug over a stronger sibling-class signal.\n */\nconst WEAK_KEYWORDS: ReadonlyArray<{ taskClass: TaskClassV2; pattern: RegExp }> = [\n\t{ taskClass: \"debug\", pattern: /\\bfix(es|ing|ed)?\\b/i },\n\t{ taskClass: \"code-gen\", pattern: /\\badd(s|ing|ed)?\\b/i },\n\t{ taskClass: \"refactor\", pattern: /\\b(simplif(y|ying|ied)|rewrit(e|ing))\\b/i },\n\t{ taskClass: \"review\", pattern: /\\b(assess(ing|ment)?|lgtm|approve)\\b/i },\n\t{ taskClass: \"plan\", pattern: /\\bbreak\\s+(this\\s+|it\\s+)?down\\b/i },\n];\n\n/** Lane fallback when no keyword/code/diff/length signal decides (mirrors v1). */\nconst LANE_FALLBACK_CLASS_V2: Record<ReasoningLaneTypeV2, TaskClassV2> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\n/**\n * Tie-break / iteration order: v1 precedence (highest first). When two classes\n * tie on score, the one appearing earlier here wins, matching v1's first-match\n * semantics. trivial is last (it carries no keyword weight under any preset).\n */\nconst PRECEDENCE_ORDER: readonly TaskClassV2[] = [\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"trivial\",\n];\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/**\n * Diff detection, identical to v1: explicit hunk headers or `diff --git` count\n * alone; bare +/- line starts only count when BOTH added and removed are present\n * (avoids false positives on markdown bullets).\n */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\nfunction clampLenBucket(len: number): number {\n\t// floor(log2(len+1)) clamped to [0,7]; integer comparison ladder, no float log.\n\tlet bucket = 0;\n\tlet v = len + 1;\n\twhile (v > 1 && bucket < 7) {\n\t\tv >>= 1;\n\t\tbucket++;\n\t}\n\treturn bucket;\n}\n\n/**\n * Caller-supplied turn input.\n *\n * - prompt: raw turn text (trimmed by the classifier).\n * - laneType: subagent lane, if any (drives lane fallback + resolver step).\n * - history: N=8 ring buffer of prior task classes, NEWEST FIRST (multi-turn).\n * - pressureBucket: context-pressure band 0..3 (see RouterFeatures).\n * - judgeVote: optional tier-2 LLM-judge label (default OFF; injected as data).\n */\nexport interface TaskClassifierInputV2 {\n\tprompt: string;\n\tlaneType?: ReasoningLaneTypeV2;\n\thistory?: readonly TaskClassV2[];\n\tpressureBucket?: number;\n\tjudgeVote?: TaskClassV2 | null;\n}\n\n/**\n * Deterministic weighted multi-signal classifier (Goal 004 Req 1).\n *\n * Pipeline:\n * 1. Extract RouterFeatures from input (STRONG/WEAK keyword 0/1 counts per\n * family, codeFence, diffHunk, lenBucket, multiTurnPrior, pressureBucket,\n * judgeVote).\n * 2. Score every class via the frozen scoreClass(c, f, weights). top1 = argmax\n * with v1-precedence tie-break; margin = top1 - top2.\n * 3. ZERO-SCORE CASCADE (v1-compat): when maxScore === 0, fall through exactly\n * like v1 steps 2-6 (fence/diff -> code-gen; len<40 -> trivial; len>=2400\n * -> plan; lane fallback; default code-gen).\n * 4. HYSTERESIS: only when weights.stickyThreshold > 0 (INACTIVE for\n * DEFAULT_WEIGHTS). If margin < stickyThreshold and history is non-empty,\n * keep the previous class unless a STRONG keyword matched for top1.\n *\n * Pure: same (input, weights) -> same TaskClassV2.\n */\nexport function classifyTaskV2(input: TaskClassifierInputV2, weights: RouterWeights): TaskClassV2 {\n\tconst prompt = input.prompt.trim();\n\n\tconst strongKeyword: Partial<Record<TaskClassV2, number>> = {};\n\tconst weakKeyword: Partial<Record<TaskClassV2, number>> = {};\n\tfor (const { taskClass, pattern } of STRONG_KEYWORDS) {\n\t\tstrongKeyword[taskClass] = pattern.test(prompt) ? 1 : 0;\n\t}\n\tfor (const { taskClass, pattern } of WEAK_KEYWORDS) {\n\t\tweakKeyword[taskClass] = (weakKeyword[taskClass] ?? 0) + (pattern.test(prompt) ? 1 : 0);\n\t}\n\n\tconst codeFence = hasCodeFence(prompt);\n\tconst diffHunk = hasDiffMarkers(prompt);\n\tconst lenBucket = clampLenBucket(prompt.length);\n\tconst history = input.history;\n\tconst multiTurnPrior: TaskClassV2 | null = history !== undefined && history.length > 0 ? history[0] : null;\n\tconst pressureBucket = input.pressureBucket ?? 0;\n\tconst judgeVote = input.judgeVote ?? null;\n\n\tconst features: RouterFeatures = {\n\t\tstrongKeyword,\n\t\tweakKeyword,\n\t\tcodeFence,\n\t\tdiffHunk,\n\t\tlenBucket,\n\t\tmultiTurnPrior,\n\t\tpressureBucket,\n\t\tjudgeVote,\n\t};\n\n\t// Score every class; track top1 (v1-precedence tie-break) and the runner-up.\n\tconst scores = new Map<TaskClassV2, number>();\n\tfor (const c of TASK_CLASSES_V2) {\n\t\tscores.set(c, scoreClass(c, features, weights));\n\t}\n\tlet top1: TaskClassV2 = PRECEDENCE_ORDER[0];\n\tfor (const c of PRECEDENCE_ORDER) {\n\t\tif ((scores.get(c) ?? 0) > (scores.get(top1) ?? 0)) top1 = c;\n\t}\n\tlet top2: TaskClassV2 | null = null;\n\tfor (const c of PRECEDENCE_ORDER) {\n\t\tif (c === top1) continue;\n\t\tif (top2 === null || (scores.get(c) ?? 0) > (scores.get(top2) ?? 0)) top2 = c;\n\t}\n\tconst maxScore = scores.get(top1) ?? 0;\n\tconst margin = top2 !== null ? maxScore - (scores.get(top2) ?? 0) : maxScore;\n\n\t// ZERO-SCORE CASCADE: no weighted signal fired -> v1 steps 2-6 verbatim.\n\tif (maxScore <= 0) {\n\t\tif (codeFence || diffHunk) return \"code-gen\";\n\t\tif (prompt.length < TRIVIAL_MAX_CHARS) return \"trivial\";\n\t\tif (prompt.length >= COMPLEX_PROSE_MIN_CHARS) return \"plan\";\n\t\tif (input.laneType) return LANE_FALLBACK_CLASS_V2[input.laneType];\n\t\treturn \"code-gen\";\n\t}\n\n\t// HYSTERESIS: keep previous class on low-margin multi-turn turns. INACTIVE for\n\t// DEFAULT_WEIGHTS (stickyThreshold === 0). A STRONG keyword hit on top1 always\n\t// bypasses stickiness so an intentional topic switch still wins.\n\tif (\n\t\tweights.stickyThreshold > 0 &&\n\t\tmargin < weights.stickyThreshold &&\n\t\tmultiTurnPrior !== null &&\n\t\t(features.strongKeyword[top1] ?? 0) <= 0\n\t) {\n\t\treturn multiTurnPrior;\n\t}\n\n\treturn top1;\n}\n\n/** Reasoning ladder used for targets and clamping. Intentionally excludes \"off\". */\nconst REASONING_LADDER_V2: readonly ThinkingLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\n\n/**\n * Static rule table: task class -> recommended ThinkingLevel (before lane\n * adjustment, bias, hint, and clamping). Identical to v1 TASK_CLASS_THINKING_LEVELS.\n */\nexport const TASK_CLASS_THINKING_LEVELS_V2: Readonly<Record<TaskClassV2, ThinkingLevel>> = {\n\ttrivial: \"minimal\",\n\t\"simple-edit\": \"low\",\n\t\"code-gen\": \"medium\",\n\tdebug: \"high\",\n\trefactor: \"high\",\n\treview: \"high\",\n\tplan: \"xhigh\",\n};\n\n/**\n * Lane ladder adjustment (mirrors DEFAULT_WEIGHTS.laneStep and v1 LANE_STEP):\n * planner/security escalate one step, explorer de-escalates one step.\n */\nconst LANE_STEP_V2: Readonly<Partial<Record<ReasoningLaneTypeV2, 1 | -1>>> = {\n\tplanner: 1,\n\tsecurity: 1,\n\texplorer: -1,\n};\n\n/** Maximum magnitude of the learning/consult bias (ladder steps). */\nconst BIAS_MAX = 2;\n/** Confidence floor at which an override hint is fused into the target. */\nconst HINT_CONFIDENCE_THRESHOLD = 0.7;\n\n/**\n * Resolve a ThinkingLevel for a task class (Goal 004 resolver).\n *\n * Pipeline: base rule table -> lane step -> bounded bias [-2,+2] -> optional hint\n * fusion (±2 bounded) -> clamp to availableLevels (v1 algorithm).\n *\n * The clamp guarantees model-capability safety: the return value is always a\n * member of availableLevels and never \"off\" for reasoning models. With\n * bias=0 and hint=null, output is identical to v1 resolveThinkingLevel.\n */\nexport function resolveThinkingLevelV2(\n\ttaskClass: TaskClassV2,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV2 | undefined,\n\tbias: number,\n\thint: { level: ThinkingLevel; confidence: number } | null,\n): ThinkingLevel {\n\tconst baseIndex = REASONING_LADDER_V2.indexOf(TASK_CLASS_THINKING_LEVELS_V2[taskClass]);\n\tconst laneStep = laneType ? (LANE_STEP_V2[laneType] ?? 0) : 0;\n\tconst biasClamped = Math.max(-BIAS_MAX, Math.min(BIAS_MAX, bias));\n\tconst ladderTop = REASONING_LADDER_V2.length - 1;\n\tlet targetIndex = Math.max(0, Math.min(baseIndex + laneStep + biasClamped, ladderTop));\n\n\t// Hint fusion (Phase A: hint is null). A valid high-confidence hint moves the\n\t// target toward hint.level by at most ±2 ladder steps (bounded blast radius).\n\tif (hint !== null && hint.confidence >= HINT_CONFIDENCE_THRESHOLD) {\n\t\tconst hintIndex = REASONING_LADDER_V2.indexOf(hint.level);\n\t\tif (hintIndex >= 0) {\n\t\t\tconst delta = hintIndex - targetIndex;\n\t\t\tconst step = Math.max(-BIAS_MAX, Math.min(BIAS_MAX, delta));\n\t\t\ttargetIndex = Math.max(0, Math.min(targetIndex + step, ladderTop));\n\t\t}\n\t}\n\n\t// Clamp to capability: highest available level <= target, else lowest available.\n\tconst availableOnLadder = REASONING_LADDER_V2.filter((level) => availableLevels.includes(level));\n\tif (availableOnLadder.length === 0) {\n\t\treturn availableLevels[0] ?? \"off\";\n\t}\n\tfor (let i = targetIndex; i >= 0; i--) {\n\t\tconst candidate = REASONING_LADDER_V2[i];\n\t\tif (availableOnLadder.includes(candidate)) return candidate;\n\t}\n\treturn availableOnLadder[0];\n}\n\n/**\n * Thin auto-mode wrapper: resolve with bias=0 and hint=null (what the session\n * calls in v2 auto thinking mode). Output equals v1 resolveThinkingLevel for the\n * same (taskClass, availableLevels, laneType).\n */\nexport function resolveThinkingLevelV2ForAuto(\n\ttaskClass: TaskClassV2,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV2 | undefined,\n): ThinkingLevel {\n\treturn resolveThinkingLevelV2(taskClass, availableLevels, laneType, 0, null);\n}\n\nexport { HISTORY_CAP };\n"]}