@remnic/core 10.0.37 → 10.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.js +76 -76
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.js +77 -77
- package/dist/access-boundary.js +76 -76
- package/dist/access-citation-namespace.js +2 -2
- package/dist/access-cli.js +165 -164
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +76 -76
- package/dist/access-http-briefing.js +77 -77
- package/dist/access-http-query.js +78 -78
- package/dist/access-http.js +101 -101
- package/dist/access-identity-continuity-surface.js +76 -76
- package/dist/access-lcm-surface.js +76 -76
- package/dist/access-mcp.js +90 -90
- package/dist/access-namespace-preflight.js +76 -76
- package/dist/access-observe-write-category.js +15 -15
- package/dist/access-observe-write-surface.js +76 -76
- package/dist/access-offline-manifest.js +12 -12
- package/dist/access-operations-batch.js +80 -80
- package/dist/access-operations.js +87 -87
- package/dist/access-recall-concurrency.js +76 -76
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +76 -76
- package/dist/access-recall-surface.js +76 -76
- package/dist/access-schema.js +10 -10
- package/dist/access-service.js +76 -76
- package/dist/access-surface-catalog.js +78 -78
- package/dist/active-recall.js +31 -31
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-R3XGJL5I.js → auto-sync-BNCBMU6H.js} +9 -9
- package/dist/briefing.js +21 -21
- package/dist/buffer-types.d.ts +59 -0
- package/dist/buffer-types.d.ts.map +1 -0
- package/dist/buffer-types.js +1 -0
- package/dist/buffer.d.ts +14 -22
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +11 -11
- package/dist/calibration.js +3 -3
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-GWVG7LGC.js} +2 -2
- package/dist/causal-behavior.js +2 -2
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +24 -24
- package/dist/causal-retrieval.js +2 -2
- package/dist/causal-trajectory-graph.js +2 -2
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-MH3LC7KB.js → chunk-22YJQTPN.js} +2 -2
- package/dist/{chunk-MKE5DP3S.js → chunk-23ZMQONL.js} +2 -2
- package/dist/{chunk-MGXFJOUR.js → chunk-2AV2U2SZ.js} +4 -4
- package/dist/{chunk-HJ3UTGIW.js → chunk-2JNBPOZ2.js} +3 -3
- package/dist/{chunk-EEOYF3IB.js → chunk-3RJDYZJE.js} +2 -2
- package/dist/{chunk-FBAHBFT5.js → chunk-4HJ2QK32.js} +2 -2
- package/dist/{chunk-UXXVOA4D.js → chunk-4LLV2N3R.js} +3 -3
- package/dist/{chunk-P5CUEB2Q.js → chunk-4QLKVZH6.js} +8 -8
- package/dist/{chunk-2RJO7VS4.js → chunk-4UT2NUAC.js} +2 -2
- package/dist/{chunk-R6TCM47S.js → chunk-5KZNWERU.js} +156 -143
- package/dist/chunk-5KZNWERU.js.map +1 -0
- package/dist/{chunk-MNDHKZZQ.js → chunk-5Q6CR65T.js} +2 -2
- package/dist/{chunk-XTBEXZFN.js → chunk-5SF7JOJB.js} +4 -4
- package/dist/{chunk-BQ6DGL6A.js → chunk-64WHWIPL.js} +2 -2
- package/dist/{chunk-TVIGN3XZ.js → chunk-6FRFVIKC.js} +10 -10
- package/dist/{chunk-A23DPFFI.js → chunk-6ITNEPJO.js} +2 -2
- package/dist/{chunk-UFBIOF7C.js → chunk-6X3J4NLZ.js} +10 -5
- package/dist/chunk-6X3J4NLZ.js.map +1 -0
- package/dist/{chunk-G6PPA4TH.js → chunk-6Z2ZWBOV.js} +6 -6
- package/dist/{chunk-QEGE5QCL.js → chunk-7IANFJ24.js} +58 -58
- package/dist/{chunk-PSQ2P2T3.js → chunk-7XZ7TOLQ.js} +3 -3
- package/dist/{chunk-Z5AQZRHF.js → chunk-AER3HD3B.js} +2 -2
- package/dist/{chunk-BPBUUGAO.js → chunk-ANV373S5.js} +2 -2
- package/dist/{chunk-GMMZ45C4.js → chunk-AVVCPA7U.js} +6 -8
- package/dist/chunk-AVVCPA7U.js.map +1 -0
- package/dist/{chunk-LSSCMZQ2.js → chunk-B3I2QQU7.js} +3 -3
- package/dist/{chunk-NSQSOLCE.js → chunk-B7VKRCAG.js} +2 -2
- package/dist/{chunk-HXFIILGF.js → chunk-BFPF5N3H.js} +4 -4
- package/dist/{chunk-KQAFEZQX.js → chunk-BUKK5SWA.js} +2 -2
- package/dist/{chunk-RBC4GIZU.js → chunk-BUKZ7MTN.js} +2 -2
- package/dist/{chunk-FO5AFCRI.js → chunk-CR72WZ3M.js} +9 -9
- package/dist/{chunk-YC2C67F7.js → chunk-CRXGQRNA.js} +3 -3
- package/dist/{chunk-WRDFYSUC.js → chunk-CZVNOTLR.js} +2 -2
- package/dist/{chunk-VPO53QMG.js → chunk-D6DOLQYK.js} +2 -2
- package/dist/{chunk-HRT2SJPC.js → chunk-E3NBMTTX.js} +2 -2
- package/dist/{chunk-HIMXDRSQ.js → chunk-E5IEI2MR.js} +2 -2
- package/dist/{chunk-Z2T5BPOZ.js → chunk-EIYJK7OD.js} +2 -2
- package/dist/{chunk-FRAL3EU4.js → chunk-EKPRLTAE.js} +2 -2
- package/dist/{chunk-EVVCE7S2.js → chunk-EKR6OO4P.js} +2 -2
- package/dist/{chunk-TYGLV6ZG.js → chunk-EYGOM6AI.js} +2 -2
- package/dist/{chunk-TYGLV6ZG.js.map → chunk-EYGOM6AI.js.map} +1 -1
- package/dist/{chunk-6R6CU3MR.js → chunk-F2IU6A2L.js} +57 -57
- package/dist/{chunk-FL6SLVAJ.js → chunk-FAKRTS3Y.js} +3 -3
- package/dist/{chunk-64IO6ZEE.js → chunk-FC3ZFEXF.js} +2 -2
- package/dist/{chunk-YGYT7S3G.js → chunk-FJ264QBP.js} +31 -9
- package/dist/chunk-FJ264QBP.js.map +1 -0
- package/dist/{chunk-DGBWPSM6.js → chunk-FLDXEJFX.js} +2 -2
- package/dist/{chunk-JH4FCVWJ.js → chunk-G6OFEGUR.js} +2 -2
- package/dist/{chunk-Z5QWUR3Y.js → chunk-GCW7UT5I.js} +2 -2
- package/dist/{chunk-ULUX6Q7R.js → chunk-GE5ZNDLE.js} +4 -4
- package/dist/{chunk-U4JXQCJ3.js → chunk-GLB6VV3J.js} +2 -2
- package/dist/{chunk-RWWYBE7E.js → chunk-GPVGAHXL.js} +3 -3
- package/dist/{chunk-3BPMYMPX.js → chunk-GSBSGT43.js} +2 -2
- package/dist/{chunk-645WOU5I.js → chunk-H7BZBSP6.js} +4 -4
- package/dist/{chunk-GFYFKXWZ.js → chunk-HUDPXTXW.js} +2 -2
- package/dist/{chunk-L2JQSG5M.js → chunk-I7TR2OCU.js} +2 -2
- package/dist/{chunk-HKEK27M4.js → chunk-IAOW2L6K.js} +2 -2
- package/dist/{chunk-STM4T335.js → chunk-IJH6METJ.js} +2 -2
- package/dist/{chunk-WADPRAZE.js → chunk-IZRNMXZC.js} +2 -2
- package/dist/{chunk-JWTUJVIF.js → chunk-JGS55BAS.js} +53 -2
- package/dist/chunk-JGS55BAS.js.map +1 -0
- package/dist/{chunk-LAQS5HYU.js → chunk-JM6U3SAE.js} +2 -2
- package/dist/{chunk-74AY3S7U.js → chunk-JNLYHJG7.js} +4 -4
- package/dist/{chunk-7ZBANI3M.js → chunk-JOOGPZJE.js} +2 -2
- package/dist/{chunk-V5RJCXLG.js → chunk-JORSMFL6.js} +6 -6
- package/dist/{chunk-VKTP3TZB.js → chunk-JQCP5QW3.js} +4 -4
- package/dist/{chunk-LRJKHXU4.js → chunk-JRTKYFFY.js} +2 -2
- package/dist/{chunk-UMFBHYPW.js → chunk-JT3IXJIL.js} +13 -13
- package/dist/{chunk-S53WZWGL.js → chunk-JZFJZLJY.js} +2 -2
- package/dist/{chunk-6QOL4G76.js → chunk-KQXRVELF.js} +2 -2
- package/dist/{chunk-USOXPN5H.js → chunk-L74NWAD4.js} +2 -2
- package/dist/{chunk-Y6Y3TRBN.js → chunk-LJXBFVI2.js} +4 -4
- package/dist/{chunk-3L5WH6CV.js → chunk-LPD6I2U6.js} +2 -2
- package/dist/{chunk-LDXS746V.js → chunk-LR7RW4T6.js} +3 -3
- package/dist/{chunk-FZVZYU5C.js → chunk-LXN737FX.js} +2 -2
- package/dist/{chunk-5U3NUHMA.js → chunk-MD3W2H3G.js} +301 -98
- package/dist/chunk-MD3W2H3G.js.map +1 -0
- package/dist/{chunk-ARFT6ZBE.js → chunk-MGYZENUY.js} +2 -2
- package/dist/{chunk-YQ6SLFNZ.js → chunk-MPJDQC4G.js} +3 -3
- package/dist/{chunk-5UWP5X3T.js → chunk-NJ2DZYEK.js} +2 -2
- package/dist/{chunk-MBMT5FUQ.js → chunk-O2O4FDAH.js} +2 -2
- package/dist/{chunk-GCVUBVEN.js → chunk-ODJ2HRFW.js} +10 -10
- package/dist/{chunk-2NLLXCJG.js → chunk-OWHERGF2.js} +2 -2
- package/dist/{chunk-I73ZSORJ.js → chunk-OXSTKCYK.js} +4 -4
- package/dist/{chunk-CNQQNYJ2.js → chunk-P3YUEPSQ.js} +2 -2
- package/dist/{chunk-A6PKEU7T.js → chunk-PARM6OYL.js} +1 -1
- package/dist/chunk-PARM6OYL.js.map +1 -0
- package/dist/{chunk-TPKB7T6Z.js → chunk-Q6UZREJK.js} +2 -2
- package/dist/{chunk-UJDTXWMT.js → chunk-QIMGULGN.js} +2 -2
- package/dist/{chunk-E24RUQEN.js → chunk-QNIYL5SK.js} +2 -2
- package/dist/{chunk-KOY25HUB.js → chunk-QYAXMXOB.js} +2 -2
- package/dist/{chunk-CU7WDDPV.js → chunk-RIAKNXZB.js} +4 -4
- package/dist/{chunk-UQWA5QDT.js → chunk-RO2NNWR5.js} +3 -3
- package/dist/{chunk-AYXRK264.js → chunk-RROFIAYI.js} +9 -9
- package/dist/{chunk-L46Y5FPE.js → chunk-RUAIO5UA.js} +2 -2
- package/dist/{chunk-QIC7YZ3D.js → chunk-RWW5KPIY.js} +18 -18
- package/dist/{chunk-E5PVMB3I.js → chunk-SDXBR5QO.js} +2 -2
- package/dist/{chunk-WSLNC5AL.js → chunk-SJHFU52W.js} +8 -8
- package/dist/{chunk-7PG7QBWD.js → chunk-SRPGA6WN.js} +2 -2
- package/dist/{chunk-CUWOSRCS.js → chunk-SUKPM6DV.js} +2 -2
- package/dist/{chunk-BBDASWNT.js → chunk-TJ5YGCPV.js} +81 -76
- package/dist/chunk-TJ5YGCPV.js.map +1 -0
- package/dist/{chunk-G2SRB62M.js → chunk-TTQYX7VU.js} +2 -2
- package/dist/{chunk-TDCR6PQL.js → chunk-TWRPWJJT.js} +2 -2
- package/dist/{chunk-KC5SVCUJ.js → chunk-U66CTIF5.js} +2 -2
- package/dist/{chunk-D2AKUKAQ.js → chunk-UIK3U36O.js} +2 -2
- package/dist/{chunk-XLEP7SS3.js → chunk-UMSH4I3E.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-UVYI6VIX.js} +1 -1
- package/dist/{chunk-LHBOFROT.js → chunk-V2XMG7GM.js} +2 -2
- package/dist/chunk-VA3GNAWQ.js +58 -0
- package/dist/chunk-VA3GNAWQ.js.map +1 -0
- package/dist/{chunk-NQOM6PHO.js → chunk-VCOCOBUS.js} +6 -6
- package/dist/{chunk-4P4VLPGB.js → chunk-VECRSYQG.js} +1 -1
- package/dist/{chunk-ID2PLENW.js → chunk-VHBGJAYC.js} +2 -2
- package/dist/{chunk-RVYXSS3Q.js → chunk-VIS23B6O.js} +8 -8
- package/dist/{chunk-NMKETOAX.js → chunk-VSHWPOUB.js} +2 -2
- package/dist/{chunk-UBN5AOLT.js → chunk-W5EO4USJ.js} +2 -2
- package/dist/{chunk-Y7427SL7.js → chunk-WCGJK4NQ.js} +4 -4
- package/dist/{chunk-CCYXCT4B.js → chunk-WQYS47G4.js} +2 -2
- package/dist/{chunk-JH2ALMPR.js → chunk-WR5POHQ3.js} +2 -2
- package/dist/{chunk-DK5VMJL3.js → chunk-WT2VVYBM.js} +2 -2
- package/dist/{chunk-EHIKY2IA.js → chunk-WU5WTCH5.js} +2 -2
- package/dist/{chunk-QKXPOLQD.js → chunk-WUBGMV3G.js} +6 -6
- package/dist/{chunk-O6YJI5VX.js → chunk-WUL6LDJ4.js} +2 -2
- package/dist/{chunk-5EILJ6W2.js → chunk-WXPBDEMW.js} +2 -2
- package/dist/{chunk-U7HJBODU.js → chunk-XKTHJHSQ.js} +5 -5
- package/dist/{chunk-JFI3ATK4.js → chunk-XMYWYJM2.js} +2 -2
- package/dist/{chunk-64S6X4IU.js → chunk-XTN54NML.js} +4 -4
- package/dist/{chunk-VGFNTY3Y.js → chunk-YFXRLRKP.js} +2 -2
- package/dist/{chunk-DAOMWGO2.js → chunk-ZX5N5D6M.js} +4 -4
- package/dist/{chunk-PTRPOJ7U.js → chunk-ZXFKG7VZ.js} +2 -2
- package/dist/cli.js +124 -124
- package/dist/coding/export-okf-codegraph.js +22 -22
- package/dist/coding/pre-action-gate.js +2 -2
- package/dist/compounding/engine.js +23 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -31
- package/dist/connectors/codex-materialize-runner.js +21 -21
- package/dist/connectors/index.js +21 -21
- package/dist/contradiction/index.js +5 -5
- package/dist/convergence-refresh.js +23 -23
- package/dist/corpus-watermark.js +23 -23
- package/dist/deep-recall-http-glue.js +11 -11
- package/dist/deep-recall-policy-llm.js +2 -2
- package/dist/deep-recall.js +2 -2
- package/dist/entity-retrieval.js +22 -22
- package/dist/event-order-recall.js +2 -2
- package/dist/explicit-capture.js +6 -6
- package/dist/external-wiki-access.js +77 -77
- package/dist/extraction-faithfulness.js +2 -2
- package/dist/extraction-judge.d.ts +42 -2
- package/dist/extraction-judge.d.ts.map +1 -1
- package/dist/extraction-judge.js +12 -2
- package/dist/extraction-window-drain.d.ts +48 -0
- package/dist/extraction-window-drain.d.ts.map +1 -0
- package/dist/extraction-window-drain.js +14 -0
- package/dist/extraction.js +29 -29
- package/dist/fallback-llm.d.ts +1 -0
- package/dist/fallback-llm.d.ts.map +1 -1
- package/dist/fallback-llm.js +1 -1
- package/dist/{graph-edge-decay-2BZTY2IM.js → graph-edge-decay-3HVLMT6U.js} +5 -5
- package/dist/graph-edge-decay-3HVLMT6U.js.map +1 -0
- package/dist/graph-jsonl.js +3 -3
- package/dist/graph-snapshot.js +3 -3
- package/dist/graph.js +4 -4
- package/dist/harmonic-retrieval.js +22 -22
- package/dist/harmonic-source-lookup.js +2 -2
- package/dist/index.js +212 -211
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +7 -7
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +24 -24
- package/dist/maintenance/memory-governance.js +21 -21
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +21 -21
- package/dist/maintenance/rebuild-memory-projection-runner.js +23 -23
- package/dist/maintenance/rebuild-memory-projection.js +22 -22
- package/dist/memory-browse-operations.js +77 -77
- package/dist/memory-browse.js +6 -6
- package/dist/memory-subject.js +3 -3
- package/dist/namespaces/migrate.js +22 -22
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.js +21 -21
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-file-io.js +2 -2
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/offline-sync-snapshot-stream.js +8 -8
- package/dist/offline-sync-upload-staging.js +3 -3
- package/dist/offline-sync.js +9 -9
- package/dist/operator-doctor-corpus.js +24 -24
- package/dist/operator-toolkit.js +42 -42
- package/dist/orchestration/extraction-drain.d.ts +90 -0
- package/dist/orchestration/extraction-drain.d.ts.map +1 -0
- package/dist/orchestration/extraction-run-helpers.d.ts +3 -0
- package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +4 -1
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +35 -35
- package/dist/orchestration/turn-ingestion.d.ts +3 -0
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.d.ts +8 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +165 -164
- package/dist/qmd.js +5 -5
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +77 -77
- package/dist/recall-navigation.js +76 -76
- package/dist/recall-planner-llm.js +1 -1
- package/dist/recall-state-view-admission.js +6 -6
- package/dist/recall-why-service.js +6 -6
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/reconcile/manifest.js +3 -3
- package/dist/{resolution-4ZEQPXUZ.js → resolution-NNYG37YB.js} +6 -6
- package/dist/resolution-NNYG37YB.js.map +1 -0
- package/dist/resume-bundles.js +32 -32
- package/dist/retrieval-agents.js +2 -2
- package/dist/salvage-envelope.js +5 -5
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +10 -10
- package/dist/secure-store/index.js +2 -2
- package/dist/semantic-consolidation.js +22 -22
- package/dist/semantic-rule-promotion.js +21 -21
- package/dist/semantic-rule-verifier.js +21 -21
- package/dist/source-agent-qualifier.js +25 -25
- package/dist/standup/build.js +21 -21
- package/dist/storage/dropped-extraction-chunk.d.ts +8 -0
- package/dist/storage/dropped-extraction-chunk.d.ts.map +1 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +20 -20
- package/dist/subject-config.js +4 -4
- package/dist/summarizer.js +4 -4
- package/dist/support-passport/index.js +79 -79
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +22 -22
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +21 -21
- package/dist/who-knows-cli.js +22 -22
- package/dist/who-knows.js +21 -21
- package/dist/write-envelope.js +4 -4
- package/package.json +2 -2
- package/src/buffer-drop-extraction-chunk.test.ts +196 -0
- package/src/buffer-types.ts +65 -0
- package/src/buffer.ts +60 -62
- package/src/config.test.ts +16 -0
- package/src/config.ts +22 -0
- package/src/extraction-judge-chain.test.ts +298 -2
- package/src/extraction-judge.ts +147 -82
- package/src/extraction-window-drain.test.ts +123 -0
- package/src/extraction-window-drain.ts +110 -0
- package/src/fallback-llm.test.ts +94 -0
- package/src/fallback-llm.ts +7 -7
- package/src/orchestration/extraction-drain.ts +261 -0
- package/src/orchestration/extraction-run-helpers.ts +8 -0
- package/src/orchestration/extraction-run.ts +5 -4
- package/src/orchestration/turn-ingestion.ts +9 -5
- package/src/orchestrator.ts +21 -0
- package/src/secure-store/secure-fs.ts +3 -1
- package/src/storage/dropped-extraction-chunk.ts +27 -0
- package/src/storage-dropped-chunk-filenames.test.ts +30 -0
- package/src/storage.ts +6 -7
- package/src/types.ts +19 -0
- package/dist/chunk-5U3NUHMA.js.map +0 -1
- package/dist/chunk-A6PKEU7T.js.map +0 -1
- package/dist/chunk-BBDASWNT.js.map +0 -1
- package/dist/chunk-GMMZ45C4.js.map +0 -1
- package/dist/chunk-JWTUJVIF.js.map +0 -1
- package/dist/chunk-R6TCM47S.js.map +0 -1
- package/dist/chunk-UFBIOF7C.js.map +0 -1
- package/dist/chunk-YGYT7S3G.js.map +0 -1
- /package/dist/{auto-sync-R3XGJL5I.js.map → auto-sync-BNCBMU6H.js.map} +0 -0
- /package/dist/{capsule-crypto-7FJQINUR.js.map → buffer-types.js.map} +0 -0
- /package/dist/{graph-edge-decay-2BZTY2IM.js.map → capsule-crypto-GWVG7LGC.js.map} +0 -0
- /package/dist/{chunk-MH3LC7KB.js.map → chunk-22YJQTPN.js.map} +0 -0
- /package/dist/{chunk-MKE5DP3S.js.map → chunk-23ZMQONL.js.map} +0 -0
- /package/dist/{chunk-MGXFJOUR.js.map → chunk-2AV2U2SZ.js.map} +0 -0
- /package/dist/{chunk-HJ3UTGIW.js.map → chunk-2JNBPOZ2.js.map} +0 -0
- /package/dist/{chunk-EEOYF3IB.js.map → chunk-3RJDYZJE.js.map} +0 -0
- /package/dist/{chunk-FBAHBFT5.js.map → chunk-4HJ2QK32.js.map} +0 -0
- /package/dist/{chunk-UXXVOA4D.js.map → chunk-4LLV2N3R.js.map} +0 -0
- /package/dist/{chunk-P5CUEB2Q.js.map → chunk-4QLKVZH6.js.map} +0 -0
- /package/dist/{chunk-2RJO7VS4.js.map → chunk-4UT2NUAC.js.map} +0 -0
- /package/dist/{chunk-MNDHKZZQ.js.map → chunk-5Q6CR65T.js.map} +0 -0
- /package/dist/{chunk-XTBEXZFN.js.map → chunk-5SF7JOJB.js.map} +0 -0
- /package/dist/{chunk-BQ6DGL6A.js.map → chunk-64WHWIPL.js.map} +0 -0
- /package/dist/{chunk-TVIGN3XZ.js.map → chunk-6FRFVIKC.js.map} +0 -0
- /package/dist/{chunk-A23DPFFI.js.map → chunk-6ITNEPJO.js.map} +0 -0
- /package/dist/{chunk-G6PPA4TH.js.map → chunk-6Z2ZWBOV.js.map} +0 -0
- /package/dist/{chunk-QEGE5QCL.js.map → chunk-7IANFJ24.js.map} +0 -0
- /package/dist/{chunk-PSQ2P2T3.js.map → chunk-7XZ7TOLQ.js.map} +0 -0
- /package/dist/{chunk-Z5AQZRHF.js.map → chunk-AER3HD3B.js.map} +0 -0
- /package/dist/{chunk-BPBUUGAO.js.map → chunk-ANV373S5.js.map} +0 -0
- /package/dist/{chunk-LSSCMZQ2.js.map → chunk-B3I2QQU7.js.map} +0 -0
- /package/dist/{chunk-NSQSOLCE.js.map → chunk-B7VKRCAG.js.map} +0 -0
- /package/dist/{chunk-HXFIILGF.js.map → chunk-BFPF5N3H.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-BUKK5SWA.js.map} +0 -0
- /package/dist/{chunk-RBC4GIZU.js.map → chunk-BUKZ7MTN.js.map} +0 -0
- /package/dist/{chunk-FO5AFCRI.js.map → chunk-CR72WZ3M.js.map} +0 -0
- /package/dist/{chunk-YC2C67F7.js.map → chunk-CRXGQRNA.js.map} +0 -0
- /package/dist/{chunk-WRDFYSUC.js.map → chunk-CZVNOTLR.js.map} +0 -0
- /package/dist/{chunk-VPO53QMG.js.map → chunk-D6DOLQYK.js.map} +0 -0
- /package/dist/{chunk-HRT2SJPC.js.map → chunk-E3NBMTTX.js.map} +0 -0
- /package/dist/{chunk-HIMXDRSQ.js.map → chunk-E5IEI2MR.js.map} +0 -0
- /package/dist/{chunk-Z2T5BPOZ.js.map → chunk-EIYJK7OD.js.map} +0 -0
- /package/dist/{chunk-FRAL3EU4.js.map → chunk-EKPRLTAE.js.map} +0 -0
- /package/dist/{chunk-EVVCE7S2.js.map → chunk-EKR6OO4P.js.map} +0 -0
- /package/dist/{chunk-6R6CU3MR.js.map → chunk-F2IU6A2L.js.map} +0 -0
- /package/dist/{chunk-FL6SLVAJ.js.map → chunk-FAKRTS3Y.js.map} +0 -0
- /package/dist/{chunk-64IO6ZEE.js.map → chunk-FC3ZFEXF.js.map} +0 -0
- /package/dist/{chunk-DGBWPSM6.js.map → chunk-FLDXEJFX.js.map} +0 -0
- /package/dist/{chunk-JH4FCVWJ.js.map → chunk-G6OFEGUR.js.map} +0 -0
- /package/dist/{chunk-Z5QWUR3Y.js.map → chunk-GCW7UT5I.js.map} +0 -0
- /package/dist/{chunk-ULUX6Q7R.js.map → chunk-GE5ZNDLE.js.map} +0 -0
- /package/dist/{chunk-U4JXQCJ3.js.map → chunk-GLB6VV3J.js.map} +0 -0
- /package/dist/{chunk-RWWYBE7E.js.map → chunk-GPVGAHXL.js.map} +0 -0
- /package/dist/{chunk-3BPMYMPX.js.map → chunk-GSBSGT43.js.map} +0 -0
- /package/dist/{chunk-645WOU5I.js.map → chunk-H7BZBSP6.js.map} +0 -0
- /package/dist/{chunk-GFYFKXWZ.js.map → chunk-HUDPXTXW.js.map} +0 -0
- /package/dist/{chunk-L2JQSG5M.js.map → chunk-I7TR2OCU.js.map} +0 -0
- /package/dist/{chunk-HKEK27M4.js.map → chunk-IAOW2L6K.js.map} +0 -0
- /package/dist/{chunk-STM4T335.js.map → chunk-IJH6METJ.js.map} +0 -0
- /package/dist/{chunk-WADPRAZE.js.map → chunk-IZRNMXZC.js.map} +0 -0
- /package/dist/{chunk-LAQS5HYU.js.map → chunk-JM6U3SAE.js.map} +0 -0
- /package/dist/{chunk-74AY3S7U.js.map → chunk-JNLYHJG7.js.map} +0 -0
- /package/dist/{chunk-7ZBANI3M.js.map → chunk-JOOGPZJE.js.map} +0 -0
- /package/dist/{chunk-V5RJCXLG.js.map → chunk-JORSMFL6.js.map} +0 -0
- /package/dist/{chunk-VKTP3TZB.js.map → chunk-JQCP5QW3.js.map} +0 -0
- /package/dist/{chunk-LRJKHXU4.js.map → chunk-JRTKYFFY.js.map} +0 -0
- /package/dist/{chunk-UMFBHYPW.js.map → chunk-JT3IXJIL.js.map} +0 -0
- /package/dist/{chunk-S53WZWGL.js.map → chunk-JZFJZLJY.js.map} +0 -0
- /package/dist/{chunk-6QOL4G76.js.map → chunk-KQXRVELF.js.map} +0 -0
- /package/dist/{chunk-USOXPN5H.js.map → chunk-L74NWAD4.js.map} +0 -0
- /package/dist/{chunk-Y6Y3TRBN.js.map → chunk-LJXBFVI2.js.map} +0 -0
- /package/dist/{chunk-3L5WH6CV.js.map → chunk-LPD6I2U6.js.map} +0 -0
- /package/dist/{chunk-LDXS746V.js.map → chunk-LR7RW4T6.js.map} +0 -0
- /package/dist/{chunk-FZVZYU5C.js.map → chunk-LXN737FX.js.map} +0 -0
- /package/dist/{chunk-ARFT6ZBE.js.map → chunk-MGYZENUY.js.map} +0 -0
- /package/dist/{chunk-YQ6SLFNZ.js.map → chunk-MPJDQC4G.js.map} +0 -0
- /package/dist/{chunk-5UWP5X3T.js.map → chunk-NJ2DZYEK.js.map} +0 -0
- /package/dist/{chunk-MBMT5FUQ.js.map → chunk-O2O4FDAH.js.map} +0 -0
- /package/dist/{chunk-GCVUBVEN.js.map → chunk-ODJ2HRFW.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-OWHERGF2.js.map} +0 -0
- /package/dist/{chunk-I73ZSORJ.js.map → chunk-OXSTKCYK.js.map} +0 -0
- /package/dist/{chunk-CNQQNYJ2.js.map → chunk-P3YUEPSQ.js.map} +0 -0
- /package/dist/{chunk-TPKB7T6Z.js.map → chunk-Q6UZREJK.js.map} +0 -0
- /package/dist/{chunk-UJDTXWMT.js.map → chunk-QIMGULGN.js.map} +0 -0
- /package/dist/{chunk-E24RUQEN.js.map → chunk-QNIYL5SK.js.map} +0 -0
- /package/dist/{chunk-KOY25HUB.js.map → chunk-QYAXMXOB.js.map} +0 -0
- /package/dist/{chunk-CU7WDDPV.js.map → chunk-RIAKNXZB.js.map} +0 -0
- /package/dist/{chunk-UQWA5QDT.js.map → chunk-RO2NNWR5.js.map} +0 -0
- /package/dist/{chunk-AYXRK264.js.map → chunk-RROFIAYI.js.map} +0 -0
- /package/dist/{chunk-L46Y5FPE.js.map → chunk-RUAIO5UA.js.map} +0 -0
- /package/dist/{chunk-QIC7YZ3D.js.map → chunk-RWW5KPIY.js.map} +0 -0
- /package/dist/{chunk-E5PVMB3I.js.map → chunk-SDXBR5QO.js.map} +0 -0
- /package/dist/{chunk-WSLNC5AL.js.map → chunk-SJHFU52W.js.map} +0 -0
- /package/dist/{chunk-7PG7QBWD.js.map → chunk-SRPGA6WN.js.map} +0 -0
- /package/dist/{chunk-CUWOSRCS.js.map → chunk-SUKPM6DV.js.map} +0 -0
- /package/dist/{chunk-G2SRB62M.js.map → chunk-TTQYX7VU.js.map} +0 -0
- /package/dist/{chunk-TDCR6PQL.js.map → chunk-TWRPWJJT.js.map} +0 -0
- /package/dist/{chunk-KC5SVCUJ.js.map → chunk-U66CTIF5.js.map} +0 -0
- /package/dist/{chunk-D2AKUKAQ.js.map → chunk-UIK3U36O.js.map} +0 -0
- /package/dist/{chunk-XLEP7SS3.js.map → chunk-UMSH4I3E.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-UVYI6VIX.js.map} +0 -0
- /package/dist/{chunk-LHBOFROT.js.map → chunk-V2XMG7GM.js.map} +0 -0
- /package/dist/{chunk-NQOM6PHO.js.map → chunk-VCOCOBUS.js.map} +0 -0
- /package/dist/{chunk-4P4VLPGB.js.map → chunk-VECRSYQG.js.map} +0 -0
- /package/dist/{chunk-ID2PLENW.js.map → chunk-VHBGJAYC.js.map} +0 -0
- /package/dist/{chunk-RVYXSS3Q.js.map → chunk-VIS23B6O.js.map} +0 -0
- /package/dist/{chunk-NMKETOAX.js.map → chunk-VSHWPOUB.js.map} +0 -0
- /package/dist/{chunk-UBN5AOLT.js.map → chunk-W5EO4USJ.js.map} +0 -0
- /package/dist/{chunk-Y7427SL7.js.map → chunk-WCGJK4NQ.js.map} +0 -0
- /package/dist/{chunk-CCYXCT4B.js.map → chunk-WQYS47G4.js.map} +0 -0
- /package/dist/{chunk-JH2ALMPR.js.map → chunk-WR5POHQ3.js.map} +0 -0
- /package/dist/{chunk-DK5VMJL3.js.map → chunk-WT2VVYBM.js.map} +0 -0
- /package/dist/{chunk-EHIKY2IA.js.map → chunk-WU5WTCH5.js.map} +0 -0
- /package/dist/{chunk-QKXPOLQD.js.map → chunk-WUBGMV3G.js.map} +0 -0
- /package/dist/{chunk-O6YJI5VX.js.map → chunk-WUL6LDJ4.js.map} +0 -0
- /package/dist/{chunk-5EILJ6W2.js.map → chunk-WXPBDEMW.js.map} +0 -0
- /package/dist/{chunk-U7HJBODU.js.map → chunk-XKTHJHSQ.js.map} +0 -0
- /package/dist/{chunk-JFI3ATK4.js.map → chunk-XMYWYJM2.js.map} +0 -0
- /package/dist/{chunk-64S6X4IU.js.map → chunk-XTN54NML.js.map} +0 -0
- /package/dist/{chunk-VGFNTY3Y.js.map → chunk-YFXRLRKP.js.map} +0 -0
- /package/dist/{chunk-DAOMWGO2.js.map → chunk-ZX5N5D6M.js.map} +0 -0
- /package/dist/{chunk-PTRPOJ7U.js.map → chunk-ZXFKG7VZ.js.map} +0 -0
- /package/dist/{resolution-4ZEQPXUZ.js.map → extraction-window-drain.js.map} +0 -0
|
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
|
|
4
4
|
import { parseConfig } from "./config.js";
|
|
5
|
-
import { judgeFactDurability } from "./extraction-judge.js";
|
|
6
|
-
import
|
|
5
|
+
import { judgeFactDurability, JUDGE_SYSTEM_PROMPT, judgeMaxTokens, MAX_JUDGE_BATCH, MAX_JUDGE_TOKENS } from "./extraction-judge.js";
|
|
6
|
+
import { FallbackLlmClient } from "./fallback-llm.js";
|
|
7
|
+
import type { FallbackLlmOptions, FallbackLlmResponse } from "./fallback-llm.js";
|
|
8
|
+
import { LocalLlmClient } from "./local-llm.js";
|
|
9
|
+
import type { LocalLlmChatCompletionOptions, LocalLlmChatCompletionResult } from "./local-llm-helpers.js";
|
|
7
10
|
|
|
8
11
|
// Regression for cursor review on #1425: the extraction judge must route gateway
|
|
9
12
|
// fallback calls through the SAME precedence as ExtractionEngine.withGatewayAgent
|
|
@@ -53,3 +56,296 @@ test("judge falls back to gatewayAgentId when no taskModelChain is set", async (
|
|
|
53
56
|
assert.equal(captured[0]?.agentId, "persona-agent");
|
|
54
57
|
assert.equal(captured[0]?.modelChain, undefined);
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
type ChatOptions = LocalLlmChatCompletionOptions;
|
|
61
|
+
|
|
62
|
+
/** A real FallbackLlmClient whose gateway call is replaced: records the options the judge passes. */
|
|
63
|
+
class RecordingFallbackLlm extends FallbackLlmClient {
|
|
64
|
+
constructor(private readonly sink: FallbackLlmOptions[]) {
|
|
65
|
+
super();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override isAvailable(): boolean {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override async chatCompletion(
|
|
73
|
+
_messages: Array<{ role: "system" | "user" | "assistant"; content: string }>,
|
|
74
|
+
options: FallbackLlmOptions = {},
|
|
75
|
+
): Promise<FallbackLlmResponse | null> {
|
|
76
|
+
this.sink.push(options);
|
|
77
|
+
return { content: JSON.stringify([{ index: 0, durable: true, reason: "ok" }]), modelUsed: "recording-fake" };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A real LocalLlmClient whose network call is replaced: records the options the judge passes. */
|
|
82
|
+
class RecordingLocalLlm extends LocalLlmClient {
|
|
83
|
+
constructor(
|
|
84
|
+
private readonly sink: ChatOptions[],
|
|
85
|
+
private readonly reply: string,
|
|
86
|
+
) {
|
|
87
|
+
super(parseConfig({}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override async chatCompletion(
|
|
91
|
+
_messages: Array<{ role: string; content: string }>,
|
|
92
|
+
options: ChatOptions = {},
|
|
93
|
+
): Promise<LocalLlmChatCompletionResult | null> {
|
|
94
|
+
this.sink.push(options);
|
|
95
|
+
return { content: this.reply };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface ExampleVerdict {
|
|
100
|
+
index?: number;
|
|
101
|
+
kind?: string;
|
|
102
|
+
reason?: string;
|
|
103
|
+
durable?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Regression (Studio 2026-10-29): the judge hardcoded timeoutMs: 1500 while the
|
|
107
|
+
// deployed local model needs tens of seconds — 94 straight aborts, 0 completions.
|
|
108
|
+
// The timeout must come from config so operators can set one the model can meet.
|
|
109
|
+
|
|
110
|
+
test("judge forwards the configured timeout to the local LLM call", async () => {
|
|
111
|
+
const config = parseConfig({ extractionJudgeTimeoutMs: 45_000 });
|
|
112
|
+
const captured: ChatOptions[] = [];
|
|
113
|
+
const localLlm = new RecordingLocalLlm(captured, JSON.stringify([{ index: 0, durable: true, reason: "ok" }]));
|
|
114
|
+
|
|
115
|
+
await judgeFactDurability([candidate], config, localLlm, null, new Map(), new Map());
|
|
116
|
+
|
|
117
|
+
assert.equal(captured.length, 1, "judge should call the local LLM once");
|
|
118
|
+
assert.equal(captured[0]?.timeoutMs, 45_000);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Compact verdict request (PR 3264 follow-up): the judge asks for a minimal
|
|
123
|
+
// `[{"index":N,"kind":"accept|reject|defer"}]` reply with a batch-scaled
|
|
124
|
+
// completion budget (24 tokens per candidate, up to 2048) instead of a flat 2048. The big budget let
|
|
125
|
+
// a verbose 59-305s reply hide inside the old 150s timeout; the compact one
|
|
126
|
+
// measures 2.1-2.5 s per call on a small fast judge model (13-22 s, 41 s
|
|
127
|
+
// verbose on the big shared core model), inside the 20s default.
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
function captureLocal(captured: ChatOptions[], content?: string): LocalLlmClient {
|
|
131
|
+
return new RecordingLocalLlm(captured, content ?? JSON.stringify([{ index: 0, kind: "reject" }]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const EIGHT_CANDIDATES = Array.from({ length: 8 }, (_, i) => ({
|
|
135
|
+
text: `sample candidate fact ${i}`,
|
|
136
|
+
category: "preference",
|
|
137
|
+
confidence: 0.5,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
test("judge uses a compact completion budget scaled to the batch size", async () => {
|
|
141
|
+
const config = parseConfig({});
|
|
142
|
+
const captured: ChatOptions[] = [];
|
|
143
|
+
|
|
144
|
+
await judgeFactDurability(EIGHT_CANDIDATES, config, captureLocal(captured), null, new Map(), new Map());
|
|
145
|
+
|
|
146
|
+
assert.equal(captured.length, 1, "judge should call the local LLM once");
|
|
147
|
+
assert.equal(captured[0]?.maxTokens, judgeMaxTokens(8));
|
|
148
|
+
assert.ok(
|
|
149
|
+
(captured[0]?.maxTokens ?? Number.POSITIVE_INFINITY) <= judgeMaxTokens(8),
|
|
150
|
+
`completion budget must stay small, got ${captured[0]?.maxTokens}`,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("judgeMaxTokens leaves room for one verdict per candidate, up to a cap", () => {
|
|
155
|
+
assert.equal(judgeMaxTokens(1), 64);
|
|
156
|
+
assert.equal(judgeMaxTokens(8), 192);
|
|
157
|
+
assert.ok(judgeMaxTokens(50) >= 50 * 16, "a 50-candidate batch needs a verdict array that fits");
|
|
158
|
+
assert.equal(judgeMaxTokens(500), 2048, "cap");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("judge bounds the whole local call, retries included, with one deadline signal", async () => {
|
|
162
|
+
const config = parseConfig({ extractionJudgeTimeoutMs: 45_000 });
|
|
163
|
+
const captured: ChatOptions[] = [];
|
|
164
|
+
|
|
165
|
+
await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
|
|
166
|
+
|
|
167
|
+
assert.ok(captured[0]?.signal instanceof AbortSignal, "judge passes an AbortSignal so retries share one deadline");
|
|
168
|
+
assert.equal(captured[0]?.signal?.aborted, false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
test("system prompt requests the compact verdict shape only", () => {
|
|
173
|
+
const lines = JUDGE_SYSTEM_PROMPT.split("\n");
|
|
174
|
+
const exampleIdx = lines.findIndex((l) => l.trim() === "Example output:");
|
|
175
|
+
assert.ok(exampleIdx >= 0, "prompt must contain an 'Example output:' line");
|
|
176
|
+
// SAFETY: the line is the example JSON checked into the prompt text this test pins.
|
|
177
|
+
const example = JSON.parse(lines[exampleIdx + 1].trim()) as { verdicts?: ExampleVerdict[] };
|
|
178
|
+
assert.ok(
|
|
179
|
+
Array.isArray(example.verdicts) && example.verdicts.length > 0,
|
|
180
|
+
'example must use the {"verdicts":[...]} wrapper — response_format json_object rejects a bare top-level array',
|
|
181
|
+
);
|
|
182
|
+
for (const item of example.verdicts) {
|
|
183
|
+
assert.ok(Number.isInteger(item.index), "example index must be an integer");
|
|
184
|
+
assert.ok(
|
|
185
|
+
item.kind === "accept" || item.kind === "reject" || item.kind === "defer",
|
|
186
|
+
`example kind must be a parser-accepted verdict, got ${item.kind}`,
|
|
187
|
+
);
|
|
188
|
+
assert.equal(item.reason, undefined, "compact request must not ask for reasons");
|
|
189
|
+
assert.equal(item.durable, undefined, "compact request must not ask for the durable flag");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("judge still accepts the legacy verbose verdict shape", async () => {
|
|
194
|
+
const config = parseConfig({});
|
|
195
|
+
const captured: ChatOptions[] = [];
|
|
196
|
+
const llm = captureLocal(
|
|
197
|
+
captured,
|
|
198
|
+
JSON.stringify([
|
|
199
|
+
{ index: 0, kind: "accept", durable: true, reason: "Stable preference" },
|
|
200
|
+
{ index: 1, durable: false, reason: "Ephemeral build status" },
|
|
201
|
+
{ index: 2, kind: "defer", durable: false, reason: "Ambiguous pronoun" },
|
|
202
|
+
]),
|
|
203
|
+
);
|
|
204
|
+
const candidates = [
|
|
205
|
+
{ text: "user prefers dark mode", category: "preference", confidence: 0.5 },
|
|
206
|
+
{ text: "the build is running now", category: "task", confidence: 0.5 },
|
|
207
|
+
{ text: "he said they'd follow up", category: "task", confidence: 0.5 },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
|
|
211
|
+
|
|
212
|
+
assert.deepEqual(
|
|
213
|
+
{ ...result.verdicts.get(0) },
|
|
214
|
+
{ durable: true, reason: "Stable preference", kind: "accept" },
|
|
215
|
+
);
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
{ ...result.verdicts.get(1) },
|
|
218
|
+
{ durable: false, reason: "Ephemeral build status" },
|
|
219
|
+
);
|
|
220
|
+
assert.deepEqual(
|
|
221
|
+
{ ...result.verdicts.get(2) },
|
|
222
|
+
{ durable: false, reason: "Ambiguous pronoun", kind: "defer" },
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("judge parses the {\"verdicts\":[...]} wrapper object the prompt requests", async () => {
|
|
227
|
+
const config = parseConfig({});
|
|
228
|
+
const captured: ChatOptions[] = [];
|
|
229
|
+
const llm = captureLocal(
|
|
230
|
+
captured,
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
verdicts: [
|
|
233
|
+
{ index: 0, kind: "accept" },
|
|
234
|
+
{ index: 1, kind: "reject" },
|
|
235
|
+
],
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const candidates = [
|
|
239
|
+
{ text: "user prefers dark mode", category: "preference", confidence: 0.5 },
|
|
240
|
+
{ text: "the build is running now", category: "task", confidence: 0.5 },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
|
|
244
|
+
|
|
245
|
+
assert.equal(result.judged, 2, "both verdicts must resolve from the wrapper");
|
|
246
|
+
assert.equal(result.verdicts.get(0)?.kind, "accept");
|
|
247
|
+
assert.equal(result.verdicts.get(1)?.kind, "reject");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("judge still parses the compact bare-array shape", async () => {
|
|
251
|
+
const config = parseConfig({});
|
|
252
|
+
const captured: ChatOptions[] = [];
|
|
253
|
+
const llm = captureLocal(
|
|
254
|
+
captured,
|
|
255
|
+
JSON.stringify([
|
|
256
|
+
{ index: 0, kind: "reject" },
|
|
257
|
+
{ index: 1, kind: "accept" },
|
|
258
|
+
]),
|
|
259
|
+
);
|
|
260
|
+
const candidates = [
|
|
261
|
+
{ text: "the build is running now", category: "task", confidence: 0.5 },
|
|
262
|
+
{ text: "user prefers dark mode", category: "preference", confidence: 0.5 },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
|
|
266
|
+
|
|
267
|
+
assert.equal(result.judged, 2, "both verdicts must resolve from the bare array");
|
|
268
|
+
assert.equal(result.verdicts.get(0)?.kind, "reject");
|
|
269
|
+
assert.equal(result.verdicts.get(1)?.kind, "accept");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("judge treats a single verdict object as one verdict at its index, not the whole batch", async () => {
|
|
273
|
+
const config = parseConfig({});
|
|
274
|
+
const captured: ChatOptions[] = [];
|
|
275
|
+
const llm = captureLocal(captured, JSON.stringify({ index: 1, kind: "reject" }));
|
|
276
|
+
const candidates = [
|
|
277
|
+
{ text: "user prefers dark mode", category: "preference", confidence: 0.5 },
|
|
278
|
+
{ text: "the build is running now", category: "task", confidence: 0.5 },
|
|
279
|
+
{ text: "he said they'd follow up", category: "task", confidence: 0.5 },
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const result = await judgeFactDurability(candidates, config, llm, null, new Map(), new Map());
|
|
283
|
+
|
|
284
|
+
assert.equal(result.judged, 1, "only the indexed candidate comes from the LLM");
|
|
285
|
+
assert.equal(result.verdicts.get(1)?.kind, "reject");
|
|
286
|
+
assert.equal(result.verdicts.get(1)?.durable, false);
|
|
287
|
+
assert.equal(result.verdicts.get(0)?.kind, undefined, "index 0 must not inherit the reject");
|
|
288
|
+
assert.equal(result.verdicts.get(0)?.durable, true, "index 0 fail-open approves");
|
|
289
|
+
assert.equal(result.verdicts.get(2)?.kind, undefined, "index 2 must not inherit the reject");
|
|
290
|
+
assert.equal(result.verdicts.get(2)?.durable, true, "index 2 fail-open approves");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Judge model override: extractionJudgeModel must reach the LLM call options
|
|
295
|
+
// (options.model) on both the local and fallback paths, and stay absent when
|
|
296
|
+
// unset so routing keeps the default model.
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
test("judge forwards extractionJudgeModel to the local LLM options", async () => {
|
|
300
|
+
const config = parseConfig({ extractionJudgeModel: "qwen3.8-27b-64k-nothink" });
|
|
301
|
+
const captured: ChatOptions[] = [];
|
|
302
|
+
|
|
303
|
+
await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
|
|
304
|
+
|
|
305
|
+
assert.equal(captured[0]?.model, "qwen3.8-27b-64k-nothink");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("judge omits model from local LLM options when extractionJudgeModel is unset", async () => {
|
|
309
|
+
const config = parseConfig({});
|
|
310
|
+
const captured: ChatOptions[] = [];
|
|
311
|
+
|
|
312
|
+
await judgeFactDurability([candidate], config, captureLocal(captured), null, new Map(), new Map());
|
|
313
|
+
|
|
314
|
+
assert.equal(captured[0]?.model, undefined);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("judge forwards extractionJudgeModel to the fallback LLM options in gateway mode", async () => {
|
|
318
|
+
const config = parseConfig({ modelSource: "gateway", extractionJudgeModel: "fast-judge" });
|
|
319
|
+
const captured: FallbackLlmOptions[] = [];
|
|
320
|
+
|
|
321
|
+
await judgeFactDurability([candidate], config, null, new RecordingFallbackLlm(captured), new Map(), new Map());
|
|
322
|
+
|
|
323
|
+
assert.equal(captured[0]?.model, "fast-judge");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("judge defaults to a 20s timeout when none is configured", async () => {
|
|
327
|
+
const config = parseConfig({});
|
|
328
|
+
const captured: FallbackLlmOptions[] = [];
|
|
329
|
+
|
|
330
|
+
await judgeFactDurability([candidate], config, null, new RecordingFallbackLlm(captured), new Map(), new Map());
|
|
331
|
+
|
|
332
|
+
assert.equal(captured[0]?.timeoutMs, 20_000);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("a configured batch size above what the token cap can answer is split", async () => {
|
|
336
|
+
const config = parseConfig({ extractionJudgeBatchSize: 200 });
|
|
337
|
+
const captured: ChatOptions[] = [];
|
|
338
|
+
const candidates = Array.from({ length: 200 }, (_, i) => ({
|
|
339
|
+
text: `distinct candidate fact number ${i}`,
|
|
340
|
+
category: "preference",
|
|
341
|
+
confidence: 0.5,
|
|
342
|
+
}));
|
|
343
|
+
|
|
344
|
+
await judgeFactDurability(candidates, config, captureLocal(captured), null, new Map(), new Map());
|
|
345
|
+
|
|
346
|
+
assert.equal(captured.length, 3, "200 candidates at 85 per request");
|
|
347
|
+
for (const call of captured) {
|
|
348
|
+
assert.ok((call.maxTokens ?? 0) <= MAX_JUDGE_TOKENS, "never above the cap");
|
|
349
|
+
}
|
|
350
|
+
assert.equal(MAX_JUDGE_BATCH * 24 <= MAX_JUDGE_TOKENS, true, "a full batch fits its own budget");
|
|
351
|
+
});
|
package/src/extraction-judge.ts
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* - Critical-importance facts are auto-approved.
|
|
11
11
|
* - Batches respect extractionJudgeBatchSize.
|
|
12
12
|
* - Content-hash caching avoids redundant LLM calls.
|
|
13
|
-
* -
|
|
13
|
+
* - LLM calls use extractionJudgeTimeoutMs (default 20s: measured 2.1-2.5 s
|
|
14
|
+
* per compact verdict request with a batch-scaled completion budget on a
|
|
15
|
+
* small fast judge model; 13-22 s per compact call, 41 s verbose, on the
|
|
16
|
+
* big shared core model — raise it there; the old 150s default only
|
|
17
|
+
* papered over 59-305s verbose replies).
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
20
|
import { createHash } from "node:crypto";
|
|
@@ -215,48 +219,79 @@ export const JUDGE_SYSTEM_PROMPT = `You are a memory curator evaluating whether
|
|
|
215
219
|
|
|
216
220
|
A fact is **durable** if it will still be useful 30+ days from now and is relevant across multiple sessions, not just the current task.
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
The user message is a JSON array of candidates: [{"index": 0, "text": "...", "category": "...", "confidence": 0.5}, ...].
|
|
219
223
|
|
|
220
|
-
|
|
221
|
-
- Personal preferences, identities, or
|
|
222
|
-
-
|
|
223
|
-
-
|
|
224
|
-
- Principles, rules, or constraints the user wants respected
|
|
225
|
-
- Stable facts about projects, tools, or workflows
|
|
226
|
-
- Commitments, deadlines, or obligations
|
|
224
|
+
Verdicts:
|
|
225
|
+
- "accept" — durable. Personal preferences, identities, relationships; decisions with rationale that affect future work; corrections to previously held beliefs; principles, rules, or constraints the user wants respected; stable facts about projects, tools, or workflows; commitments, deadlines, or obligations.
|
|
226
|
+
- "reject" — not durable. Transient task details ("currently debugging line 42"); ephemeral state ("the build is running now"); routine operations ("ran npm install"); conversational filler; anything stale within hours; step-by-step instructions for a one-time task.
|
|
227
|
+
- "defer" — MIGHT be durable but the candidate alone cannot say: ambiguous referents ("he said they'd follow up"), in-progress statements that may become durable once completed, future commitments with unclear subject or timeline. Defer is NOT a soft reject: use it only when another turn of context would genuinely change the verdict; when torn between accept and reject, lean accept — losing a useful fact is worse than keeping a marginal one.
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- Ephemeral state ("the build is running now")
|
|
231
|
-
- Routine operations ("ran npm install")
|
|
232
|
-
- Conversational filler or acknowledgements
|
|
233
|
-
- Information that will be stale within hours
|
|
234
|
-
- Step-by-step instructions for a one-time task
|
|
229
|
+
Reply with a JSON object with a single "verdicts" key, exactly one object per input candidate:
|
|
230
|
+
{"verdicts": [{"index": <candidate index>, "kind": "accept" | "reject" | "defer"}]}
|
|
235
231
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
232
|
+
Rules:
|
|
233
|
+
1. The top level must be one object {"verdicts": [...]} — JSON mode rejects a bare top-level array.
|
|
234
|
+
2. Copy each object's "index" from the input candidate. One object per candidate, exactly.
|
|
235
|
+
3. No other fields — no "reason", no "durable", no commentary.
|
|
236
|
+
4. Output valid JSON only. No markdown fences.
|
|
237
|
+
5. ${OUTPUT_LANGUAGE_POLICY}
|
|
241
238
|
|
|
242
|
-
|
|
239
|
+
Example output:
|
|
240
|
+
{"verdicts": [{"index": 0, "kind": "accept"}, {"index": 1, "kind": "reject"}, {"index": 2, "kind": "defer"}]}`;
|
|
243
241
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
- reason: string (brief explanation, under 80 characters)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Judge request assembly (PR 3264 follow-up)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
248
245
|
|
|
249
|
-
|
|
246
|
+
/** Completion tokens reserved per candidate verdict (measured 13 on a small judge model). */
|
|
247
|
+
const JUDGE_TOKENS_PER_CANDIDATE = 24;
|
|
248
|
+
/** Ceiling on one judge reply: the old flat budget. */
|
|
249
|
+
export const MAX_JUDGE_TOKENS = 2048;
|
|
250
|
+
/**
|
|
251
|
+
* Extra completion tokens for a judge call routed through the Responses API. Reasoning tokens count against
|
|
252
|
+
* `max_output_tokens`, so the compact verdict budget alone can be spent before a reasoning-enabled model answers; the
|
|
253
|
+
* unparsed batch would then fail open as durable. A cap, not a target: a non-reasoning model still writes a short reply.
|
|
254
|
+
*/
|
|
255
|
+
export const JUDGE_REASONING_HEADROOM_TOKENS = 1024;
|
|
256
|
+
/** Largest batch whose verdict array fits MAX_JUDGE_TOKENS; a larger configured batch is split. */
|
|
257
|
+
export const MAX_JUDGE_BATCH = Math.floor(MAX_JUDGE_TOKENS / JUDGE_TOKENS_PER_CANDIDATE);
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Compact completion budget for one judge call. A verdict is
|
|
261
|
+
* `{"index":12,"kind":"reject"}` (~14 tokens), so the old flat 2048 only gave
|
|
262
|
+
* a slow model room to produce 59-305s replies that hid inside the timeout.
|
|
263
|
+
* Scales with the batch size (about 24 tokens per candidate verdict, measured
|
|
264
|
+
* 13 per verdict on a small judge model) so a large batch is never truncated;
|
|
265
|
+
* capped at 2048, the old flat budget.
|
|
266
|
+
*/
|
|
267
|
+
export function judgeMaxTokens(batchSize: number): number {
|
|
268
|
+
return Math.min(MAX_JUDGE_TOKENS, Math.max(64, batchSize * JUDGE_TOKENS_PER_CANDIDATE));
|
|
269
|
+
}
|
|
257
270
|
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
/** One indexed candidate entry in a judge request payload. */
|
|
272
|
+
export interface JudgeRequestEntry {
|
|
273
|
+
index: number;
|
|
274
|
+
text: string;
|
|
275
|
+
category: string;
|
|
276
|
+
confidence: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Full judge LLM request: messages plus the completion budget. */
|
|
280
|
+
export interface JudgeRequest {
|
|
281
|
+
messages: Array<{ role: "system" | "user"; content: string }>;
|
|
282
|
+
maxTokens: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Build a full judge request body: messages plus the completion budget. */
|
|
286
|
+
export function buildJudgeRequest(entries: readonly JudgeRequestEntry[]): JudgeRequest {
|
|
287
|
+
return {
|
|
288
|
+
messages: [
|
|
289
|
+
{ role: "system", content: JUDGE_SYSTEM_PROMPT },
|
|
290
|
+
{ role: "user", content: JSON.stringify(entries) },
|
|
291
|
+
],
|
|
292
|
+
maxTokens: judgeMaxTokens(entries.length),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
260
295
|
|
|
261
296
|
// ---------------------------------------------------------------------------
|
|
262
297
|
// Content-hash cache (in-memory, per-process fallback)
|
|
@@ -492,40 +527,39 @@ export async function judgeFactDurability(
|
|
|
492
527
|
}
|
|
493
528
|
|
|
494
529
|
// Batch the pending candidates up to batchSize
|
|
495
|
-
const batchSize = config.extractionJudgeBatchSize;
|
|
530
|
+
const batchSize = Math.min(config.extractionJudgeBatchSize, MAX_JUDGE_BATCH);
|
|
531
|
+
// Defer bookkeeping spans the WHOLE call, not one batch: identical candidates can land in different batches, and each key
|
|
532
|
+
// must advance the defer cap once and decide against the same prior count (the snapshot taken at its first sighting).
|
|
533
|
+
const deferredThisCall = new Set<string>();
|
|
534
|
+
const priorDefersSeen = new Map<string, number>();
|
|
496
535
|
for (let batchStart = 0; batchStart < pendingIndices.length; batchStart += batchSize) {
|
|
497
536
|
const batchIndices = pendingIndices.slice(batchStart, batchStart + batchSize);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
537
|
+
const request = buildJudgeRequest(
|
|
538
|
+
batchIndices.map((idx) => ({
|
|
539
|
+
index: idx,
|
|
540
|
+
text: candidates[idx].text,
|
|
541
|
+
category: candidates[idx].category,
|
|
542
|
+
confidence: candidates[idx].confidence,
|
|
543
|
+
})),
|
|
544
|
+
);
|
|
506
545
|
|
|
507
546
|
try {
|
|
508
|
-
const llmResponse = await callJudgeLlm(
|
|
509
|
-
userPrompt,
|
|
510
|
-
config,
|
|
511
|
-
localLlm,
|
|
512
|
-
fallbackLlm,
|
|
513
|
-
);
|
|
547
|
+
const llmResponse = await callJudgeLlm(request, config, localLlm, fallbackLlm);
|
|
514
548
|
|
|
515
549
|
if (llmResponse) {
|
|
516
550
|
const parsed = parseJudgeResponse(llmResponse, batchIndices);
|
|
517
|
-
// Per-
|
|
551
|
+
// Per-call defer-increment dedupe (codex P2): a single extraction
|
|
518
552
|
// pass must not advance the cap more than once for identical
|
|
519
|
-
// candidate content, even if the same text appears
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
const deferredThisBatch = new Set<string>();
|
|
553
|
+
// candidate content, even if the same text appears several times in
|
|
554
|
+
// one LLM response or in different batches of one call. Duplicate
|
|
555
|
+
// hits share the first sighting's `priorDeferrals` snapshot.
|
|
523
556
|
for (const [idx, rawVerdict] of parsed.entries()) {
|
|
524
557
|
const c = candidates[idx];
|
|
525
558
|
const key = cacheKey(c.text, c.category);
|
|
526
559
|
let verdict = rawVerdict;
|
|
527
560
|
let source: JudgeVerdictObservation["source"] = "llm";
|
|
528
|
-
|
|
561
|
+
if (!priorDefersSeen.has(key)) priorDefersSeen.set(key, deferCountMap.get(key) ?? 0);
|
|
562
|
+
const priorDefers = priorDefersSeen.get(key) ?? 0;
|
|
529
563
|
|
|
530
564
|
// Defer cap (issue #562, PR 2). A candidate that has already been
|
|
531
565
|
// deferred `deferCap` times is forcibly rejected so a pathological
|
|
@@ -541,15 +575,15 @@ export async function judgeFactDurability(
|
|
|
541
575
|
// Only clear + count the cap conversion once per batch for
|
|
542
576
|
// this key — duplicates in the same response all resolve to
|
|
543
577
|
// reject but should not inflate the cap-rejection counter.
|
|
544
|
-
if (!
|
|
578
|
+
if (!deferredThisCall.has(key)) {
|
|
545
579
|
deferCountMap.delete(key);
|
|
546
580
|
deferredCappedToReject++;
|
|
547
|
-
|
|
581
|
+
deferredThisCall.add(key);
|
|
548
582
|
}
|
|
549
|
-
} else if (!
|
|
583
|
+
} else if (!deferredThisCall.has(key)) {
|
|
550
584
|
deferCountMap.set(key, priorDefers + 1);
|
|
551
585
|
deferred++;
|
|
552
|
-
|
|
586
|
+
deferredThisCall.add(key);
|
|
553
587
|
// Bound the per-process defer-counter map.
|
|
554
588
|
if (deferCountMap.size > DEFER_COUNT_MAX_SIZE) {
|
|
555
589
|
const drop = Math.floor(deferCountMap.size / 2);
|
|
@@ -561,12 +595,14 @@ export async function judgeFactDurability(
|
|
|
561
595
|
}
|
|
562
596
|
}
|
|
563
597
|
}
|
|
564
|
-
// else: duplicate defer for a key already counted this
|
|
598
|
+
// else: duplicate defer for a key already counted this call —
|
|
565
599
|
// no additional counter increment, verdict still marked defer.
|
|
566
600
|
} else {
|
|
567
601
|
// On accept/reject, clear any outstanding defer counter so a
|
|
568
602
|
// future reappearance of the same text starts fresh.
|
|
569
603
|
deferCountMap.delete(key);
|
|
604
|
+
priorDefersSeen.set(key, 0);
|
|
605
|
+
deferredThisCall.delete(key);
|
|
570
606
|
}
|
|
571
607
|
|
|
572
608
|
verdicts.set(idx, verdict);
|
|
@@ -607,6 +643,8 @@ export async function judgeFactDurability(
|
|
|
607
643
|
// telemetry event reflects the true state rather than always 0.
|
|
608
644
|
const priorDefers = deferCountMap.get(hash) ?? 0;
|
|
609
645
|
deferCountMap.delete(hash);
|
|
646
|
+
priorDefersSeen.set(hash, 0);
|
|
647
|
+
deferredThisCall.delete(hash);
|
|
610
648
|
const v: JudgeVerdict = {
|
|
611
649
|
durable: true,
|
|
612
650
|
reason: "Approved by default (judge unavailable or parse error)",
|
|
@@ -641,15 +679,12 @@ export async function judgeFactDurability(
|
|
|
641
679
|
// ---------------------------------------------------------------------------
|
|
642
680
|
|
|
643
681
|
async function callJudgeLlm(
|
|
644
|
-
|
|
682
|
+
request: JudgeRequest,
|
|
645
683
|
config: PluginConfig,
|
|
646
684
|
localLlm: LocalLlmClient | null,
|
|
647
685
|
fallbackLlm: FallbackLlmClient | null,
|
|
648
686
|
): Promise<string | null> {
|
|
649
|
-
const
|
|
650
|
-
{ role: "system", content: JUDGE_SYSTEM_PROMPT },
|
|
651
|
-
{ role: "user", content: userPrompt },
|
|
652
|
-
];
|
|
687
|
+
const { messages, maxTokens } = request;
|
|
653
688
|
|
|
654
689
|
const modelOverride = config.extractionJudgeModel || undefined;
|
|
655
690
|
|
|
@@ -669,9 +704,14 @@ async function callJudgeLlm(
|
|
|
669
704
|
try {
|
|
670
705
|
const result = await (localLlm as any).chatCompletion(messages, {
|
|
671
706
|
temperature: 0.1,
|
|
672
|
-
maxTokens
|
|
707
|
+
maxTokens,
|
|
708
|
+
// A verdict needs no reasoning: keep a thinking-capable model from spending the compact budget on it.
|
|
709
|
+
disableThinking: true,
|
|
673
710
|
responseFormat: { type: "json_object" },
|
|
674
|
-
timeoutMs:
|
|
711
|
+
timeoutMs: config.extractionJudgeTimeoutMs,
|
|
712
|
+
// The client applies timeoutMs per attempt and retries an abort once:
|
|
713
|
+
// one deadline signal bounds the whole call to the configured time.
|
|
714
|
+
signal: AbortSignal.timeout(config.extractionJudgeTimeoutMs),
|
|
675
715
|
operation: "extraction-judge",
|
|
676
716
|
...(modelOverride ? { model: modelOverride } : {}),
|
|
677
717
|
});
|
|
@@ -692,8 +732,9 @@ async function callJudgeLlm(
|
|
|
692
732
|
messages as Array<{ role: "system" | "user" | "assistant"; content: string }>,
|
|
693
733
|
{
|
|
694
734
|
temperature: 0.1,
|
|
695
|
-
maxTokens
|
|
696
|
-
|
|
735
|
+
maxTokens,
|
|
736
|
+
reasoningHeadroomTokens: JUDGE_REASONING_HEADROOM_TOKENS,
|
|
737
|
+
timeoutMs: config.extractionJudgeTimeoutMs,
|
|
697
738
|
...(modelOverride ? { model: modelOverride } : {}),
|
|
698
739
|
...gatewayChain,
|
|
699
740
|
},
|
|
@@ -711,6 +752,38 @@ async function callJudgeLlm(
|
|
|
711
752
|
return null;
|
|
712
753
|
}
|
|
713
754
|
|
|
755
|
+
interface JudgeVerdictItem {
|
|
756
|
+
index?: number;
|
|
757
|
+
kind?: string;
|
|
758
|
+
action?: string;
|
|
759
|
+
durable?: boolean;
|
|
760
|
+
reason?: string;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** What a judge model may return: a bare array, or one object wrapping verdicts. */
|
|
764
|
+
interface JudgeEnvelope {
|
|
765
|
+
verdicts?: JudgeVerdictItem[];
|
|
766
|
+
index?: number;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
type ParsedJudgeJson = JudgeVerdictItem[] | JudgeEnvelope | null | undefined;
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Normalise the object shapes a judge model returns to a verdict array.
|
|
773
|
+
* Requested shape {"verdicts": [...]} (response_format json_object forces a
|
|
774
|
+
* top-level object); a single verdict object counts as one verdict at its
|
|
775
|
+
* index, never the whole batch; legacy: any array-valued key.
|
|
776
|
+
*/
|
|
777
|
+
function unwrapJudgeEnvelope(parsed: ParsedJudgeJson): JudgeVerdictItem[] | null {
|
|
778
|
+
if (!parsed || Array.isArray(parsed)) return null;
|
|
779
|
+
if (Array.isArray(parsed.verdicts)) return parsed.verdicts;
|
|
780
|
+
if (Number.isInteger(parsed.index)) return [parsed];
|
|
781
|
+
for (const v of Object.values(parsed)) {
|
|
782
|
+
if (Array.isArray(v)) return v;
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
|
|
714
787
|
// ---------------------------------------------------------------------------
|
|
715
788
|
// Response parsing
|
|
716
789
|
// ---------------------------------------------------------------------------
|
|
@@ -724,7 +797,7 @@ function parseJudgeResponse(
|
|
|
724
797
|
|
|
725
798
|
try {
|
|
726
799
|
// Try direct parse first, then fall back to JSON extraction
|
|
727
|
-
let parsed:
|
|
800
|
+
let parsed: ParsedJudgeJson;
|
|
728
801
|
try {
|
|
729
802
|
parsed = JSON.parse(raw);
|
|
730
803
|
} catch {
|
|
@@ -735,20 +808,12 @@ function parseJudgeResponse(
|
|
|
735
808
|
}
|
|
736
809
|
|
|
737
810
|
if (!Array.isArray(parsed)) {
|
|
738
|
-
|
|
739
|
-
if (
|
|
740
|
-
const values = Object.values(parsed as Record<string, unknown>);
|
|
741
|
-
for (const v of values) {
|
|
742
|
-
if (Array.isArray(v)) {
|
|
743
|
-
parsed = v;
|
|
744
|
-
break;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (!Array.isArray(parsed)) {
|
|
811
|
+
const unwrapped = unwrapJudgeEnvelope(parsed);
|
|
812
|
+
if (unwrapped === null) {
|
|
749
813
|
log.debug("extraction-judge: response is not an array, cannot parse");
|
|
750
814
|
return result;
|
|
751
815
|
}
|
|
816
|
+
parsed = unwrapped;
|
|
752
817
|
}
|
|
753
818
|
|
|
754
819
|
for (const item of parsed) {
|