@remnic/core 9.3.712 → 9.3.714
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-boundary.d.ts +6 -6
- package/dist/access-boundary.js +17 -16
- package/dist/access-cli.js +40 -39
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +24 -23
- package/dist/access-mcp.d.ts +18 -5
- package/dist/access-mcp.js +21 -20
- package/dist/access-operations-batch.js +18 -17
- package/dist/access-operations.d.ts +6 -5
- package/dist/access-operations.js +20 -19
- package/dist/access-schema.d.ts +10 -0
- package/dist/access-schema.js +1 -1
- package/dist/{access-service-Dx2rSJjH.d.ts → access-service-lddjZoRh.d.ts} +20 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +16 -15
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-surface-catalog.js +6 -1
- package/dist/access-surface-catalog.js.map +1 -1
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +3 -1
- package/dist/active-memory-bridge.js +2 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +4 -3
- package/dist/active-recall.js.map +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +8 -7
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CadPvH29.d.ts → catalog-CbR0CsUw.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +9 -8
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-OMSTU233.js → chunk-2KAYTPPT.js} +4 -4
- package/dist/{chunk-VYZNWH4J.js → chunk-2SXMCQEQ.js} +3 -3
- package/dist/{chunk-NXL5CVE7.js → chunk-2Y6LMOQQ.js} +2 -2
- package/dist/{chunk-AMXOCZHE.js → chunk-3VUPWDSE.js} +15 -3
- package/dist/{chunk-AMXOCZHE.js.map → chunk-3VUPWDSE.js.map} +1 -1
- package/dist/{chunk-FVQJYWH7.js → chunk-5GKPFL3S.js} +1 -1
- package/dist/{chunk-FVQJYWH7.js.map → chunk-5GKPFL3S.js.map} +1 -1
- package/dist/{chunk-LQ4J7ELC.js → chunk-67HG7KFE.js} +2 -2
- package/dist/{chunk-ZBG4K36I.js → chunk-6FUJ2VYY.js} +3 -3
- package/dist/{chunk-GMRNKPWO.js → chunk-6QUPUTT7.js} +2 -2
- package/dist/{chunk-Z56IHRVV.js → chunk-6UU43ZGD.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-OWOKOFXK.js → chunk-AJOIM6UR.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-YVWZW37D.js → chunk-BNOAB5HH.js} +62 -12
- package/dist/{chunk-YVWZW37D.js.map → chunk-BNOAB5HH.js.map} +1 -1
- package/dist/{chunk-A2R5NV6O.js → chunk-CTAHKIG6.js} +205 -29
- package/dist/chunk-CTAHKIG6.js.map +1 -0
- package/dist/{chunk-K22IJO4R.js → chunk-CTOQEZSN.js} +2 -2
- package/dist/{chunk-SINGJCUR.js → chunk-CX77GNMB.js} +14 -3
- package/dist/chunk-CX77GNMB.js.map +1 -0
- package/dist/{chunk-HSDJCT3V.js → chunk-DXGS5YOQ.js} +3 -3
- package/dist/{chunk-HV57RHMD.js → chunk-EH5ED3C6.js} +2 -2
- package/dist/{chunk-NUWZFSKC.js → chunk-FVZ2ISIX.js} +9 -1
- package/dist/chunk-FVZ2ISIX.js.map +1 -0
- package/dist/{chunk-GKPIY7EQ.js → chunk-FXZKXW7C.js} +117 -46
- package/dist/chunk-FXZKXW7C.js.map +1 -0
- package/dist/{chunk-7NDYFAJS.js → chunk-GM3BMWKR.js} +2 -2
- package/dist/{chunk-ZVDRL6SK.js → chunk-HHLWSWWF.js} +2 -2
- package/dist/chunk-HHLWSWWF.js.map +1 -0
- package/dist/{chunk-5QUS24F7.js → chunk-I56LKMF7.js} +2 -2
- package/dist/{chunk-Z3B2YW56.js → chunk-IHMT6XTY.js} +9 -5
- package/dist/chunk-IHMT6XTY.js.map +1 -0
- package/dist/chunk-JAKIDQB2.js +173 -0
- package/dist/chunk-JAKIDQB2.js.map +1 -0
- package/dist/{chunk-35YJ6KCV.js → chunk-JEIROMLX.js} +2 -2
- package/dist/{chunk-SKQCFAYU.js → chunk-JVEMUX6L.js} +2 -2
- package/dist/{chunk-CXMXAC5R.js → chunk-K44MIN6D.js} +3 -3
- package/dist/{chunk-5KQCOIPW.js → chunk-MECU6WSH.js} +188 -31
- package/dist/chunk-MECU6WSH.js.map +1 -0
- package/dist/{chunk-BQCFXAMN.js → chunk-MPHDVHIB.js} +2 -2
- package/dist/{chunk-BOGENF7P.js → chunk-MSVSP5VO.js} +1 -1
- package/dist/chunk-MSVSP5VO.js.map +1 -0
- package/dist/{chunk-QXNFQKWU.js → chunk-MTHNJCOJ.js} +2 -2
- package/dist/{chunk-3TCRU4JA.js → chunk-O62P2LFL.js} +2 -2
- package/dist/{chunk-JSDZMOT7.js → chunk-OC56UWV5.js} +11 -11
- package/dist/{chunk-7IBEWQLG.js → chunk-QOIONSOI.js} +2 -2
- package/dist/{chunk-4P5UAL3C.js → chunk-RBHANGVX.js} +6 -6
- package/dist/{chunk-SK2CR6MW.js → chunk-RVLB4E6N.js} +2 -2
- package/dist/{chunk-I3BT2IDW.js → chunk-SYDHFFAW.js} +1110 -28
- package/dist/chunk-SYDHFFAW.js.map +1 -0
- package/dist/{chunk-VILEUJXC.js → chunk-UC4YOAC2.js} +74 -2
- package/dist/{chunk-VILEUJXC.js.map → chunk-UC4YOAC2.js.map} +1 -1
- package/dist/{chunk-CNVIWMQI.js → chunk-UY4JMEAK.js} +2 -2
- package/dist/{chunk-S6FQLQGH.js → chunk-V7GDTQ6I.js} +2 -2
- package/dist/{chunk-JNOYYWCA.js → chunk-VO4UICII.js} +3 -3
- package/dist/{chunk-54TI5GLV.js → chunk-WI4QX7GH.js} +3 -3
- package/dist/{chunk-3XHD3XGK.js → chunk-XCTDHF7U.js} +30 -2
- package/dist/chunk-XCTDHF7U.js.map +1 -0
- package/dist/{chunk-XKU4YE6Z.js → chunk-XZTUMWKQ.js} +2 -2
- package/dist/{cli-DTWRoJEI.d.ts → cli-iAiVkbYJ.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +43 -42
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +8 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -3
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +8 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +9 -8
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +8 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +3 -2
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +4 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +62 -61
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +3 -3
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +8 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -8
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +12 -11
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +8 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +10 -9
- package/dist/{orchestrator-B1iVjEyc.d.ts → orchestrator-iVZWi8aW.d.ts} +21 -4
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +32 -31
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/provenance.js +3 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-handles.d.ts +228 -0
- package/dist/recall-handles.js +42 -0
- package/dist/recall-handles.js.map +1 -0
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +40 -2
- package/dist/recall-state.js +3 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +5 -4
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +6 -2
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +4 -4
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-CY8uVRNJ.d.ts → semantic-consolidation-DSjMDZ1u.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +9 -8
- package/dist/semantic-rule-promotion.js +8 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +8 -7
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +3 -3
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +7 -6
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/{types-Dr0Cw3L9.d.ts → types-D9dwzU_x.d.ts} +25 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +8 -7
- package/package.json +2 -2
- package/src/access-boundary.ts +4 -1
- package/src/access-http.ts +78 -20
- package/src/access-mcp.test.ts +29 -0
- package/src/access-mcp.ts +99 -17
- package/src/access-operations-batch.ts +14 -0
- package/src/access-operations.ts +5 -0
- package/src/access-schema.ts +8 -0
- package/src/access-service-observe-idempotency.test.ts +308 -0
- package/src/access-service.ts +106 -5
- package/src/access-surface-catalog.test.ts +4 -2
- package/src/access-surface-catalog.ts +5 -0
- package/src/active-memory-bridge.test.ts +72 -0
- package/src/active-memory-bridge.ts +21 -2
- package/src/chat/chat-cli.test.ts +142 -0
- package/src/chat/chat-cli.ts +206 -0
- package/src/chat/chat-config.ts +57 -0
- package/src/chat/chat-engine.test.ts +272 -0
- package/src/chat/chat-engine.ts +518 -0
- package/src/chat/chat-executor.ts +133 -0
- package/src/chat/chat-factory.ts +161 -0
- package/src/chat/chat-http.test.ts +197 -0
- package/src/chat/chat-http.ts +222 -0
- package/src/chat/chat-llm.ts +229 -0
- package/src/chat/chat-mcp.test.ts +135 -0
- package/src/chat/chat-security.test.ts +491 -0
- package/src/chat/chat-session.ts +296 -0
- package/src/chat/chat-system-prompt.ts +76 -0
- package/src/chat/chat-tools.ts +255 -0
- package/src/chat/chat-types.ts +167 -0
- package/src/cli.ts +20 -0
- package/src/config.ts +9 -0
- package/src/orchestrator.ts +106 -5
- package/src/recall-handles-wiring.test.ts +477 -0
- package/src/recall-handles.test.ts +283 -0
- package/src/recall-handles.ts +414 -0
- package/src/recall-state.ts +99 -0
- package/src/sanitize.ts +6 -0
- package/src/types.ts +29 -0
- package/dist/chunk-3XHD3XGK.js.map +0 -1
- package/dist/chunk-5KQCOIPW.js.map +0 -1
- package/dist/chunk-A2R5NV6O.js.map +0 -1
- package/dist/chunk-BOGENF7P.js.map +0 -1
- package/dist/chunk-GKPIY7EQ.js.map +0 -1
- package/dist/chunk-I3BT2IDW.js.map +0 -1
- package/dist/chunk-NUWZFSKC.js.map +0 -1
- package/dist/chunk-SINGJCUR.js.map +0 -1
- package/dist/chunk-Z3B2YW56.js.map +0 -1
- package/dist/chunk-ZVDRL6SK.js.map +0 -1
- /package/dist/{chunk-OMSTU233.js.map → chunk-2KAYTPPT.js.map} +0 -0
- /package/dist/{chunk-VYZNWH4J.js.map → chunk-2SXMCQEQ.js.map} +0 -0
- /package/dist/{chunk-NXL5CVE7.js.map → chunk-2Y6LMOQQ.js.map} +0 -0
- /package/dist/{chunk-LQ4J7ELC.js.map → chunk-67HG7KFE.js.map} +0 -0
- /package/dist/{chunk-ZBG4K36I.js.map → chunk-6FUJ2VYY.js.map} +0 -0
- /package/dist/{chunk-GMRNKPWO.js.map → chunk-6QUPUTT7.js.map} +0 -0
- /package/dist/{chunk-Z56IHRVV.js.map → chunk-6UU43ZGD.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-OWOKOFXK.js.map → chunk-AJOIM6UR.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-K22IJO4R.js.map → chunk-CTOQEZSN.js.map} +0 -0
- /package/dist/{chunk-HSDJCT3V.js.map → chunk-DXGS5YOQ.js.map} +0 -0
- /package/dist/{chunk-HV57RHMD.js.map → chunk-EH5ED3C6.js.map} +0 -0
- /package/dist/{chunk-7NDYFAJS.js.map → chunk-GM3BMWKR.js.map} +0 -0
- /package/dist/{chunk-5QUS24F7.js.map → chunk-I56LKMF7.js.map} +0 -0
- /package/dist/{chunk-35YJ6KCV.js.map → chunk-JEIROMLX.js.map} +0 -0
- /package/dist/{chunk-SKQCFAYU.js.map → chunk-JVEMUX6L.js.map} +0 -0
- /package/dist/{chunk-CXMXAC5R.js.map → chunk-K44MIN6D.js.map} +0 -0
- /package/dist/{chunk-BQCFXAMN.js.map → chunk-MPHDVHIB.js.map} +0 -0
- /package/dist/{chunk-QXNFQKWU.js.map → chunk-MTHNJCOJ.js.map} +0 -0
- /package/dist/{chunk-3TCRU4JA.js.map → chunk-O62P2LFL.js.map} +0 -0
- /package/dist/{chunk-JSDZMOT7.js.map → chunk-OC56UWV5.js.map} +0 -0
- /package/dist/{chunk-7IBEWQLG.js.map → chunk-QOIONSOI.js.map} +0 -0
- /package/dist/{chunk-4P5UAL3C.js.map → chunk-RBHANGVX.js.map} +0 -0
- /package/dist/{chunk-SK2CR6MW.js.map → chunk-RVLB4E6N.js.map} +0 -0
- /package/dist/{chunk-CNVIWMQI.js.map → chunk-UY4JMEAK.js.map} +0 -0
- /package/dist/{chunk-S6FQLQGH.js.map → chunk-V7GDTQ6I.js.map} +0 -0
- /package/dist/{chunk-JNOYYWCA.js.map → chunk-VO4UICII.js.map} +0 -0
- /package/dist/{chunk-54TI5GLV.js.map → chunk-WI4QX7GH.js.map} +0 -0
- /package/dist/{chunk-XKU4YE6Z.js.map → chunk-XZTUMWKQ.js.map} +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat engine — the bounded tool-loop agent (issue #1583).
|
|
3
|
+
*
|
|
4
|
+
* Loop: user message → LLM (with tool schema) → execute tool calls → feed
|
|
5
|
+
* results back → final reply. Max `chat.maxToolCallsPerTurn` tool calls per
|
|
6
|
+
* user turn; budget exhausted → reply with partial results + what was
|
|
7
|
+
* skipped (rule 34 — never a silent stall).
|
|
8
|
+
*
|
|
9
|
+
* Confirmation protocol (the safety core): mutating tools
|
|
10
|
+
* (correction_apply, memory_promote) are two-phase in the engine itself,
|
|
11
|
+
* independent of the LLM's judgment. The engine intercepts a mutation tool
|
|
12
|
+
* call, checks the session's confirmedPlanIds set; absent → the engine
|
|
13
|
+
* returns the plan preview to the user with a confirmation prompt; the
|
|
14
|
+
* user's affirmative (exact-match: yes/y/apply/confirm — deterministic, not
|
|
15
|
+
* LLM-interpreted) marks it confirmed for ONE apply.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
ChatLlmAdapter,
|
|
22
|
+
ChatLlmMessage,
|
|
23
|
+
ChatSessionState,
|
|
24
|
+
ChatToolResult,
|
|
25
|
+
ChatToolSchema,
|
|
26
|
+
ChatTurnResult,
|
|
27
|
+
} from "./chat-types.js";
|
|
28
|
+
import { buildChatSystemPrompt } from "./chat-system-prompt.js";
|
|
29
|
+
import {
|
|
30
|
+
buildChatToolSchemas,
|
|
31
|
+
MUTATING_TOOLS,
|
|
32
|
+
type ChatToolName,
|
|
33
|
+
} from "./chat-tools.js";
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Tool executor — delegates each tool to access-service (rule 22)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The executor interface the engine calls for each tool. Each implementation
|
|
41
|
+
* is a thin adapter over the corresponding access-service method — zero new
|
|
42
|
+
* business logic lives in the engine.
|
|
43
|
+
*/
|
|
44
|
+
export interface ChatToolExecutor {
|
|
45
|
+
memorySearch(query: string, maxResults?: number): Promise<string>;
|
|
46
|
+
memoryGet(memoryId: string): Promise<string>;
|
|
47
|
+
memoryTimeline(memoryId: string, limit?: number): Promise<string>;
|
|
48
|
+
recallExplain(query: string): Promise<string>;
|
|
49
|
+
entityGet(name: string): Promise<string>;
|
|
50
|
+
stats(): Promise<string>;
|
|
51
|
+
reviewList(runId?: string): Promise<string>;
|
|
52
|
+
scopeInspect(): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Generate a correction plan. Returns a JSON string with a planId and
|
|
55
|
+
* a diff preview. When the Correction Contract (#1580) is not merged,
|
|
56
|
+
* this is a stub that returns a not-available marker.
|
|
57
|
+
*/
|
|
58
|
+
correctionPlan(request: string): Promise<{ planId: string; preview: string }>;
|
|
59
|
+
/**
|
|
60
|
+
* Apply a correction plan. Returns a JSON result string.
|
|
61
|
+
*/
|
|
62
|
+
correctionApply(planId: string): Promise<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Promote a memory's tier. Returns a JSON result string.
|
|
65
|
+
*/
|
|
66
|
+
memoryPromote(memoryId: string): Promise<string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Engine options
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export interface ChatEngineOptions {
|
|
74
|
+
llm: ChatLlmAdapter;
|
|
75
|
+
executor: ChatToolExecutor;
|
|
76
|
+
maxToolCallsPerTurn: number;
|
|
77
|
+
model?: string;
|
|
78
|
+
/** Whether the Correction Contract (#1580) plan/apply is available. */
|
|
79
|
+
correctionAvailable: boolean;
|
|
80
|
+
/** Whether the scope resolver (#1494) is available. */
|
|
81
|
+
scopeInspectAvailable: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Confirmation keywords — deterministic, not LLM-interpreted
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const CONFIRMATION_KEYWORDS = new Set(["yes", "y", "apply", "confirm"]);
|
|
89
|
+
|
|
90
|
+
export function isConfirmationMessage(text: string): boolean {
|
|
91
|
+
return CONFIRMATION_KEYWORDS.has(text.trim().toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Engine
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export class ChatEngine {
|
|
99
|
+
private readonly llm: ChatLlmAdapter;
|
|
100
|
+
private readonly executor: ChatToolExecutor;
|
|
101
|
+
private readonly maxToolCallsPerTurn: number;
|
|
102
|
+
private readonly model?: string;
|
|
103
|
+
private readonly correctionAvailable: boolean;
|
|
104
|
+
private readonly scopeInspectAvailable: boolean;
|
|
105
|
+
private readonly systemPrompt: string;
|
|
106
|
+
private readonly toolSchemas: ChatToolSchema[];
|
|
107
|
+
private readonly allowedToolNames: Set<string>;
|
|
108
|
+
|
|
109
|
+
constructor(opts: ChatEngineOptions) {
|
|
110
|
+
this.llm = opts.llm;
|
|
111
|
+
this.executor = opts.executor;
|
|
112
|
+
this.maxToolCallsPerTurn = opts.maxToolCallsPerTurn;
|
|
113
|
+
this.model = opts.model;
|
|
114
|
+
this.correctionAvailable = opts.correctionAvailable;
|
|
115
|
+
this.scopeInspectAvailable = opts.scopeInspectAvailable;
|
|
116
|
+
this.systemPrompt = buildChatSystemPrompt({
|
|
117
|
+
correctionAvailable: opts.correctionAvailable,
|
|
118
|
+
});
|
|
119
|
+
this.toolSchemas = buildChatToolSchemas({
|
|
120
|
+
correctionAvailable: opts.correctionAvailable,
|
|
121
|
+
scopeInspectAvailable: opts.scopeInspectAvailable,
|
|
122
|
+
});
|
|
123
|
+
// Pre-compute the set of allowed tool names for fast lookup (cursor HIGH —
|
|
124
|
+
// executeTool must not run a tool that's not in the active schema set).
|
|
125
|
+
this.allowedToolNames = new Set(this.toolSchemas.map((t) => t.function.name));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Process a single user message. The session state is mutated in place
|
|
130
|
+
* (confirmedPlanIds, pendingPlanId) — the caller persists the transcript.
|
|
131
|
+
*/
|
|
132
|
+
async processMessage(
|
|
133
|
+
userMessage: string,
|
|
134
|
+
session: ChatSessionState,
|
|
135
|
+
): Promise<ChatTurnResult> {
|
|
136
|
+
// ── Confirmation fast-path ──────────────────────────────────────────
|
|
137
|
+
// If there is a pending plan and the user's message is an exact-match
|
|
138
|
+
// confirmation keyword, mark the plan confirmed and apply it directly
|
|
139
|
+
// (bypassing the LLM — deterministic, not LLM-interpreted).
|
|
140
|
+
if (
|
|
141
|
+
isConfirmationMessage(userMessage) &&
|
|
142
|
+
this.correctionAvailable
|
|
143
|
+
) {
|
|
144
|
+
// Confirm + apply a pending correction plan.
|
|
145
|
+
if (session.pendingPlanId) {
|
|
146
|
+
const planId = session.pendingPlanId;
|
|
147
|
+
session.confirmedPlanIds.add(planId);
|
|
148
|
+
session.pendingPlanId = undefined;
|
|
149
|
+
try {
|
|
150
|
+
const applyResult = await this.executor.correctionApply(planId);
|
|
151
|
+
// Consume the one-time confirmation on success so a later
|
|
152
|
+
// tool-loop correction_apply cannot re-apply the same plan id
|
|
153
|
+
// (cursor Medium: stale confirm allowed re-apply).
|
|
154
|
+
session.confirmedPlanIds.delete(planId);
|
|
155
|
+
return {
|
|
156
|
+
reply: `Correction applied.\n\n${applyResult}`,
|
|
157
|
+
chatSessionId: session.id,
|
|
158
|
+
...(session.pendingPromotionId ? {} : {}),
|
|
159
|
+
};
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Roll back: the plan was NOT applied — restore the pending state
|
|
162
|
+
// so the user can retry (cursor HIGH OlACo).
|
|
163
|
+
session.confirmedPlanIds.delete(planId);
|
|
164
|
+
session.pendingPlanId = planId;
|
|
165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
166
|
+
return {
|
|
167
|
+
reply: "[error] Failed to apply the correction. Please try again or re-request the plan.",
|
|
168
|
+
chatSessionId: session.id,
|
|
169
|
+
error: msg,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Confirm + apply a pending memory promotion.
|
|
174
|
+
if (session.pendingPromotionId) {
|
|
175
|
+
const memoryId = session.pendingPromotionId;
|
|
176
|
+
session.pendingPromotionId = undefined;
|
|
177
|
+
try {
|
|
178
|
+
const promoteResult = await this.executor.memoryPromote(memoryId);
|
|
179
|
+
return {
|
|
180
|
+
reply: `Memory promoted.\n\n${promoteResult}`,
|
|
181
|
+
chatSessionId: session.id,
|
|
182
|
+
};
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// Roll back: the promotion was NOT applied — restore pending state.
|
|
185
|
+
session.pendingPromotionId = memoryId;
|
|
186
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
187
|
+
return {
|
|
188
|
+
reply: "[error] Failed to promote the memory. Please try again.",
|
|
189
|
+
chatSessionId: session.id,
|
|
190
|
+
error: msg,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Normal tool-loop ────────────────────────────────────────────────
|
|
197
|
+
// Build the transcript context, filtering out:
|
|
198
|
+
// - the system header (seq === 0)
|
|
199
|
+
// - internal state markers (pending_plan:/pending_promotion:/plan_applied:)
|
|
200
|
+
// - the last user entry if it matches userMessage (the caller already
|
|
201
|
+
// appended it to the transcript before calling processMessage — adding
|
|
202
|
+
// it again below would duplicate it in the prompt).
|
|
203
|
+
const recentEntries = session.transcript
|
|
204
|
+
.slice(-21)
|
|
205
|
+
.filter((e) => e.role !== "system" || e.seq !== 0)
|
|
206
|
+
.filter((e) => !(e.role === "system" && /^[a-z_]+:/.test(e.content)))
|
|
207
|
+
.filter((e, _i, arr) => {
|
|
208
|
+
// Drop the last user entry if it duplicates the current message.
|
|
209
|
+
if (e.role === "user" && e.content === userMessage) {
|
|
210
|
+
const lastUser = [...arr].reverse().find((x) => x.role === "user");
|
|
211
|
+
return e !== lastUser;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
});
|
|
215
|
+
const conversation: ChatLlmMessage[] = [
|
|
216
|
+
{ role: "system", content: this.systemPrompt },
|
|
217
|
+
...recentEntries.map((e) => ({
|
|
218
|
+
role: e.role as "user" | "assistant" | "tool",
|
|
219
|
+
content: e.content,
|
|
220
|
+
...(e.toolCallId ? { toolCallId: e.toolCallId } : {}),
|
|
221
|
+
})),
|
|
222
|
+
{ role: "user", content: userMessage },
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
let toolCallCount = 0;
|
|
226
|
+
const skippedTools: string[] = [];
|
|
227
|
+
let pendingPlan: { planId: string; preview: string } | undefined;
|
|
228
|
+
|
|
229
|
+
for (;;) {
|
|
230
|
+
let response;
|
|
231
|
+
try {
|
|
232
|
+
response = await this.llm.complete(conversation, {
|
|
233
|
+
tools: this.toolSchemas,
|
|
234
|
+
...(this.model ? { model: this.model } : {}),
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
238
|
+
return {
|
|
239
|
+
reply: "[error] The model request failed. Check your model configuration and retry.",
|
|
240
|
+
chatSessionId: session.id,
|
|
241
|
+
error: msg,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// LLM outage (rule 13) — tagged error reply.
|
|
246
|
+
if (!response) {
|
|
247
|
+
return {
|
|
248
|
+
reply: "[error] No LLM model is available. Configure a local or cloud model to use Remnic Chat.",
|
|
249
|
+
chatSessionId: session.id,
|
|
250
|
+
error: "no_llm_available",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// No tool calls → final reply.
|
|
255
|
+
if (response.toolCalls.length === 0) {
|
|
256
|
+
// Citation guard: if the reply asserts memory content without tool
|
|
257
|
+
// results in this turn, append a citation-missing warning.
|
|
258
|
+
let reply = response.content;
|
|
259
|
+
if (toolCallCount === 0 && looksLikeMemoryAssertion(reply)) {
|
|
260
|
+
reply += "\n\n⚠️ The above assertion was not grounded in a tool result this turn. Please verify with memory_search or memory_get.";
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
reply,
|
|
264
|
+
chatSessionId: session.id,
|
|
265
|
+
...(pendingPlan ? { pendingPlan } : {}),
|
|
266
|
+
...(skippedTools.length > 0 ? { skippedTools } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Push the assistant response content once for this LLM turn (not per
|
|
271
|
+
// tool call — avoids duplicate assistant messages in the conversation).
|
|
272
|
+
conversation.push({ role: "assistant", content: response.content });
|
|
273
|
+
|
|
274
|
+
// Execute each tool call (budget-limited).
|
|
275
|
+
for (const tc of response.toolCalls) {
|
|
276
|
+
if (toolCallCount >= this.maxToolCallsPerTurn) {
|
|
277
|
+
skippedTools.push(tc.name);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
toolCallCount++;
|
|
281
|
+
|
|
282
|
+
// ── Tool-schema gate (cursor HIGH OlCjs — must run before mutating-tool
|
|
283
|
+
// interception so disabled tools like correction_apply/memory_promote
|
|
284
|
+
// are rejected before any confirmation flow or executor call).
|
|
285
|
+
if (!this.allowedToolNames.has(tc.name)) {
|
|
286
|
+
conversation.push({
|
|
287
|
+
role: "tool",
|
|
288
|
+
content: JSON.stringify({ error: `Tool '${tc.name}' is not available in this configuration.` }),
|
|
289
|
+
toolCallId: tc.id,
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Confirmation protocol for mutating tools ──────────────────
|
|
295
|
+
if (MUTATING_TOOLS.has(tc.name as ChatToolName)) {
|
|
296
|
+
if (tc.name === "correction_apply") {
|
|
297
|
+
const planId = typeof tc.arguments.planId === "string" ? tc.arguments.planId : "";
|
|
298
|
+
if (!session.confirmedPlanIds.has(planId)) {
|
|
299
|
+
// Not confirmed — return the plan preview for user confirmation.
|
|
300
|
+
// The plan was generated by a prior correction_plan call; fetch it.
|
|
301
|
+
try {
|
|
302
|
+
const plan = await this.executor.correctionPlan(
|
|
303
|
+
`retrieve plan ${planId}`,
|
|
304
|
+
);
|
|
305
|
+
// Bind the REQUESTED planId so the confirmation fast-path applies
|
|
306
|
+
// the exact plan shown (not an executor re-minted id).
|
|
307
|
+
pendingPlan = { planId, preview: plan.preview };
|
|
308
|
+
session.pendingPlanId = planId;
|
|
309
|
+
conversation.push({
|
|
310
|
+
role: "tool",
|
|
311
|
+
content: JSON.stringify({
|
|
312
|
+
intercepted: true,
|
|
313
|
+
message: "This mutation requires confirmation. The plan preview has been shown to the user.",
|
|
314
|
+
}),
|
|
315
|
+
toolCallId: tc.id,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
reply: formatPlanPreview(plan.preview),
|
|
319
|
+
chatSessionId: session.id,
|
|
320
|
+
pendingPlan,
|
|
321
|
+
};
|
|
322
|
+
} catch {
|
|
323
|
+
conversation.push({
|
|
324
|
+
role: "tool",
|
|
325
|
+
content: JSON.stringify({ error: "Plan not found or expired." }),
|
|
326
|
+
toolCallId: tc.id,
|
|
327
|
+
});
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Confirmed — consume the one-time confirmation.
|
|
332
|
+
session.confirmedPlanIds.delete(planId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── memory_promote confirmation (mutating tool) ─────────────────
|
|
337
|
+
// memory_promote is NEVER executed directly in the tool loop — it can
|
|
338
|
+
// only be applied via the confirmation fast-path at the top of
|
|
339
|
+
// processMessage (cursor HIGH: prevent bypass via repeated tool calls).
|
|
340
|
+
if (tc.name === "memory_promote" && this.correctionAvailable) {
|
|
341
|
+
const memoryId = typeof tc.arguments.memoryId === "string" ? tc.arguments.memoryId : "";
|
|
342
|
+
session.pendingPromotionId = memoryId;
|
|
343
|
+
conversation.push({
|
|
344
|
+
role: "tool",
|
|
345
|
+
content: JSON.stringify({
|
|
346
|
+
intercepted: true,
|
|
347
|
+
message: `Memory promotion requires confirmation. Reply 'apply' to promote memory '${memoryId}'.`,
|
|
348
|
+
}),
|
|
349
|
+
toolCallId: tc.id,
|
|
350
|
+
});
|
|
351
|
+
return {
|
|
352
|
+
reply: `I want to promote memory \`${memoryId}\`. Reply **apply** to confirm this promotion, or **cancel** to discard it.`,
|
|
353
|
+
chatSessionId: session.id,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Execute the tool ───────────────────────────────────────────
|
|
358
|
+
let result: ChatToolResult;
|
|
359
|
+
try {
|
|
360
|
+
result = await this.executeTool(tc.name, tc.arguments, session);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
363
|
+
result = { content: JSON.stringify({ error: msg }) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Feed the tool result back into the conversation.
|
|
367
|
+
conversation.push({
|
|
368
|
+
role: "tool",
|
|
369
|
+
content: result.content,
|
|
370
|
+
toolCallId: tc.id,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// If a correction_plan surfaced, track the pending plan.
|
|
374
|
+
if (tc.name === "correction_plan" && this.correctionAvailable) {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(result.content) as { planId?: string; preview?: string };
|
|
377
|
+
if (parsed.planId && parsed.preview) {
|
|
378
|
+
pendingPlan = { planId: parsed.planId, preview: parsed.preview };
|
|
379
|
+
session.pendingPlanId = parsed.planId;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
// The plan result might be a stub — that's fine.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// If we hit the budget and there are still tool calls, report partial.
|
|
388
|
+
if (skippedTools.length > 0) {
|
|
389
|
+
// Ask the LLM for a final summary with what we have.
|
|
390
|
+
conversation.push({
|
|
391
|
+
role: "user",
|
|
392
|
+
content: `[system] Tool budget exhausted (${this.maxToolCallsPerTurn} calls). ${skippedTools.length} tool(s) were skipped: ${skippedTools.join(", ")}. Please summarize what you found.`,
|
|
393
|
+
});
|
|
394
|
+
// One more LLM call for the summary, then we're done.
|
|
395
|
+
let summaryResponse;
|
|
396
|
+
try {
|
|
397
|
+
summaryResponse = await this.llm.complete(conversation, {
|
|
398
|
+
tools: [], // No more tools — force a text reply.
|
|
399
|
+
...(this.model ? { model: this.model } : {}),
|
|
400
|
+
});
|
|
401
|
+
} catch {
|
|
402
|
+
// Budget-exhaustion summary is best-effort — don't mask the partial
|
|
403
|
+
// results with a transport error (kilo review, Thread 16).
|
|
404
|
+
summaryResponse = null;
|
|
405
|
+
}
|
|
406
|
+
const summaryText = summaryResponse?.content ?? "I reached my tool-call budget. Here is what I found so far, but some queries were skipped.";
|
|
407
|
+
return {
|
|
408
|
+
reply: summaryText,
|
|
409
|
+
chatSessionId: session.id,
|
|
410
|
+
...(pendingPlan ? { pendingPlan } : {}),
|
|
411
|
+
skippedTools,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Execute a single tool by name, delegating to the executor.
|
|
419
|
+
*/
|
|
420
|
+
private async executeTool(
|
|
421
|
+
name: string,
|
|
422
|
+
args: Record<string, unknown>,
|
|
423
|
+
_session: ChatSessionState,
|
|
424
|
+
): Promise<ChatToolResult> {
|
|
425
|
+
switch (name as ChatToolName) {
|
|
426
|
+
case "memory_search": {
|
|
427
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
428
|
+
const maxResults = typeof args.maxResults === "number" ? args.maxResults : undefined;
|
|
429
|
+
return { content: await this.executor.memorySearch(query, maxResults) };
|
|
430
|
+
}
|
|
431
|
+
case "memory_get": {
|
|
432
|
+
const memoryId = typeof args.memoryId === "string" ? args.memoryId : "";
|
|
433
|
+
return { content: await this.executor.memoryGet(memoryId) };
|
|
434
|
+
}
|
|
435
|
+
case "memory_timeline": {
|
|
436
|
+
const memoryId = typeof args.memoryId === "string" ? args.memoryId : "";
|
|
437
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
438
|
+
return { content: await this.executor.memoryTimeline(memoryId, limit) };
|
|
439
|
+
}
|
|
440
|
+
case "recall_explain": {
|
|
441
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
442
|
+
return { content: await this.executor.recallExplain(query) };
|
|
443
|
+
}
|
|
444
|
+
case "entity_get": {
|
|
445
|
+
const entityName = typeof args.name === "string" ? args.name : "";
|
|
446
|
+
return { content: await this.executor.entityGet(entityName) };
|
|
447
|
+
}
|
|
448
|
+
case "stats": {
|
|
449
|
+
return { content: await this.executor.stats() };
|
|
450
|
+
}
|
|
451
|
+
case "correction_plan": {
|
|
452
|
+
const request = typeof args.request === "string" ? args.request : "";
|
|
453
|
+
const plan = await this.executor.correctionPlan(request);
|
|
454
|
+
return { content: JSON.stringify(plan) };
|
|
455
|
+
}
|
|
456
|
+
case "correction_apply": {
|
|
457
|
+
const planId = typeof args.planId === "string" ? args.planId : "";
|
|
458
|
+
return { content: await this.executor.correctionApply(planId) };
|
|
459
|
+
}
|
|
460
|
+
case "memory_promote": {
|
|
461
|
+
const memoryId = typeof args.memoryId === "string" ? args.memoryId : "";
|
|
462
|
+
return { content: await this.executor.memoryPromote(memoryId) };
|
|
463
|
+
}
|
|
464
|
+
case "review_list": {
|
|
465
|
+
const runId = typeof args.runId === "string" ? args.runId : undefined;
|
|
466
|
+
return { content: await this.executor.reviewList(runId) };
|
|
467
|
+
}
|
|
468
|
+
case "scope_inspect": {
|
|
469
|
+
return { content: await this.executor.scopeInspect() };
|
|
470
|
+
}
|
|
471
|
+
default:
|
|
472
|
+
return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Helpers
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Heuristic: does the reply look like it's asserting stored memory content
|
|
483
|
+
* without citing a tool result? This is intentionally simple — flag, don't
|
|
484
|
+
* police perfectly (per the issue's pitfall note).
|
|
485
|
+
*/
|
|
486
|
+
function looksLikeMemoryAssertion(text: string): boolean {
|
|
487
|
+
const lower = text.toLowerCase();
|
|
488
|
+
// Phrases that indicate a memory assertion.
|
|
489
|
+
const assertionPhrases = [
|
|
490
|
+
"you remember",
|
|
491
|
+
"i remember",
|
|
492
|
+
"according to my memory",
|
|
493
|
+
"your memory",
|
|
494
|
+
"stored memory",
|
|
495
|
+
"remnic remembers",
|
|
496
|
+
"remnic knows",
|
|
497
|
+
];
|
|
498
|
+
// Only flag if there's an assertion AND no citation marker.
|
|
499
|
+
const hasAssertion = assertionPhrases.some((p) => lower.includes(p));
|
|
500
|
+
const hasCitation = lower.includes("[id:") || lower.includes("memory id") || lower.includes("source:");
|
|
501
|
+
return hasAssertion && !hasCitation;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Format a plan preview for display to the user.
|
|
506
|
+
*/
|
|
507
|
+
function formatPlanPreview(preview: string): string {
|
|
508
|
+
return [
|
|
509
|
+
"Here is the correction plan I generated:",
|
|
510
|
+
"",
|
|
511
|
+
"```diff",
|
|
512
|
+
preview,
|
|
513
|
+
"```",
|
|
514
|
+
"",
|
|
515
|
+
"Reply **apply** to confirm and apply this correction.",
|
|
516
|
+
"Reply **cancel** to discard it.",
|
|
517
|
+
].join("\n");
|
|
518
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production chat-tool executor (issue #1583).
|
|
3
|
+
*
|
|
4
|
+
* Each method is a thin adapter over the corresponding EngramAccessService
|
|
5
|
+
* method — zero new business logic (rule 22). The executor carries the
|
|
6
|
+
* session's principal/namespace/sessionKey so every tool call flows through
|
|
7
|
+
* access-service with the caller's identity (rule 42).
|
|
8
|
+
*
|
|
9
|
+
* Correction tools (correction_plan / correction_apply / memory_promote) are
|
|
10
|
+
* stubbed behind the same interface until the Correction Contract (#1580)
|
|
11
|
+
* merges. The stub returns a clear not-available marker so the engine and
|
|
12
|
+
* surfaces degrade gracefully.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
16
|
+
import type { ChatToolExecutor } from "./chat-engine.js";
|
|
17
|
+
|
|
18
|
+
export interface ChatExecutorBindings {
|
|
19
|
+
service: EngramAccessService;
|
|
20
|
+
principal?: string;
|
|
21
|
+
namespace?: string;
|
|
22
|
+
sessionKey?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a production executor bound to the caller's identity.
|
|
27
|
+
*/
|
|
28
|
+
export function createChatExecutor(bindings: ChatExecutorBindings): ChatToolExecutor {
|
|
29
|
+
const { service, principal, namespace, sessionKey } = bindings;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
async memorySearch(query: string, maxResults?: number): Promise<string> {
|
|
33
|
+
const result = await service.memorySearch({
|
|
34
|
+
query,
|
|
35
|
+
...(namespace ? { namespace } : {}),
|
|
36
|
+
...(maxResults !== undefined ? { maxResults } : {}),
|
|
37
|
+
...(principal ? { principal } : {}),
|
|
38
|
+
});
|
|
39
|
+
return JSON.stringify(result);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async memoryGet(memoryId: string): Promise<string> {
|
|
43
|
+
const result = await service.memoryGet(memoryId, namespace, principal, sessionKey);
|
|
44
|
+
return JSON.stringify(result);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async memoryTimeline(memoryId: string, limit?: number): Promise<string> {
|
|
48
|
+
const result = await service.memoryTimeline(memoryId, namespace, limit ?? 200, principal);
|
|
49
|
+
return JSON.stringify(result);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async recallExplain(query: string): Promise<string> {
|
|
53
|
+
const result = await service.recallExplain({
|
|
54
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
55
|
+
...(namespace ? { namespace } : {}),
|
|
56
|
+
...(principal ? { authenticatedPrincipal: principal } : {}),
|
|
57
|
+
...(query ? { query } : {}),
|
|
58
|
+
});
|
|
59
|
+
return JSON.stringify(result);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async entityGet(name: string): Promise<string> {
|
|
63
|
+
const result = await service.entityGet(name, namespace);
|
|
64
|
+
return JSON.stringify(result);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async stats(): Promise<string> {
|
|
68
|
+
const [profile, entities, questions] = await Promise.all([
|
|
69
|
+
service.memoryProfile(namespace, principal),
|
|
70
|
+
service.memoryEntitiesList(namespace, principal),
|
|
71
|
+
service.memoryQuestions(namespace, principal),
|
|
72
|
+
]);
|
|
73
|
+
return JSON.stringify({ profile, entities, questions });
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async reviewList(runId?: string): Promise<string> {
|
|
77
|
+
const result = await service.reviewQueue(runId, namespace, principal);
|
|
78
|
+
return JSON.stringify(result);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async scopeInspect(): Promise<string> {
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
namespace: namespace ?? "default",
|
|
84
|
+
principal: principal ?? "default",
|
|
85
|
+
sessionKey: sessionKey ?? undefined,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// ── Correction tools — stubbed until #1580 merges ──────────────────
|
|
90
|
+
// When the Correction Contract lands, replace these stubs with calls to
|
|
91
|
+
// the correction module's plan/apply interface. The engine, CLI, and
|
|
92
|
+
// MCP tool all consume this interface, so the stub-to-real swap is a
|
|
93
|
+
// single-file change here.
|
|
94
|
+
|
|
95
|
+
async correctionPlan(request: string): Promise<{ planId: string; preview: string }> {
|
|
96
|
+
// Stub: the Correction Contract (#1580) is not yet merged.
|
|
97
|
+
// Return a not-available marker that the engine surfaces gracefully.
|
|
98
|
+
return {
|
|
99
|
+
planId: `stub-${Date.now()}`,
|
|
100
|
+
preview: `[Correction Contract (#1580) not yet available]\n\nRequested correction: "${request}"\n\nThe plan/apply pipeline is pending. Inspection tools are fully functional.`,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async correctionApply(planId: string): Promise<string> {
|
|
105
|
+
// Stub: no-op until #1580 merges.
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
applied: false,
|
|
108
|
+
planId,
|
|
109
|
+
reason: "Correction Contract (#1580) not yet available. No mutation was applied.",
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async memoryPromote(memoryId: string): Promise<string> {
|
|
114
|
+
// Delegate to the existing memoryPromote service method if available.
|
|
115
|
+
try {
|
|
116
|
+
const serviceWithPromote = service as EngramAccessService & {
|
|
117
|
+
memoryPromote?: (memoryId: string, namespace?: string, principal?: string) => Promise<unknown>;
|
|
118
|
+
};
|
|
119
|
+
if (typeof serviceWithPromote.memoryPromote === "function") {
|
|
120
|
+
const result = await serviceWithPromote.memoryPromote(memoryId, namespace, principal);
|
|
121
|
+
return JSON.stringify(result);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Fall through to stub.
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
promoted: false,
|
|
128
|
+
memoryId,
|
|
129
|
+
reason: "Memory promotion is not available in this build.",
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|