@ai-agent-forge/plugin-memory 0.88.0 → 0.88.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +10 -4
  2. package/agent-forge.json +1 -1
  3. package/bin/agent-forge-memory-inference.js +8 -0
  4. package/dist/capability.d.ts +42 -59
  5. package/dist/capability.d.ts.map +1 -1
  6. package/dist/capability.js +263 -2055
  7. package/dist/capability.js.map +1 -1
  8. package/dist/entry.d.ts +11 -0
  9. package/dist/entry.d.ts.map +1 -1
  10. package/dist/entry.js +38 -2
  11. package/dist/entry.js.map +1 -1
  12. package/dist/index.d.ts +5 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +7 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/inference-bin.d.ts +21 -0
  17. package/dist/inference-bin.d.ts.map +1 -0
  18. package/dist/inference-bin.js +85 -0
  19. package/dist/inference-bin.js.map +1 -0
  20. package/dist/memory/command-face.d.ts +86 -0
  21. package/dist/memory/command-face.d.ts.map +1 -0
  22. package/dist/memory/command-face.js +377 -0
  23. package/dist/memory/command-face.js.map +1 -0
  24. package/dist/memory/embedding-provider.d.ts.map +1 -1
  25. package/dist/memory/embedding-provider.js +14 -2
  26. package/dist/memory/embedding-provider.js.map +1 -1
  27. package/dist/memory/embedding-reranker.d.ts.map +1 -1
  28. package/dist/memory/embedding-reranker.js +14 -2
  29. package/dist/memory/embedding-reranker.js.map +1 -1
  30. package/dist/memory/enabling.d.ts +104 -0
  31. package/dist/memory/enabling.d.ts.map +1 -0
  32. package/dist/memory/enabling.js +321 -0
  33. package/dist/memory/enabling.js.map +1 -0
  34. package/dist/memory/hooks.d.ts +110 -0
  35. package/dist/memory/hooks.d.ts.map +1 -0
  36. package/dist/memory/hooks.js +478 -0
  37. package/dist/memory/hooks.js.map +1 -0
  38. package/dist/memory/inference-channel.d.ts +33 -0
  39. package/dist/memory/inference-channel.d.ts.map +1 -0
  40. package/dist/memory/inference-channel.js +101 -0
  41. package/dist/memory/inference-channel.js.map +1 -0
  42. package/dist/memory/inference-client.d.ts +25 -0
  43. package/dist/memory/inference-client.d.ts.map +1 -0
  44. package/dist/memory/inference-client.js +216 -0
  45. package/dist/memory/inference-client.js.map +1 -0
  46. package/dist/memory/inference-protocol.d.ts +95 -0
  47. package/dist/memory/inference-protocol.d.ts.map +1 -0
  48. package/dist/memory/inference-protocol.js +27 -0
  49. package/dist/memory/inference-protocol.js.map +1 -0
  50. package/dist/memory/inference-server.d.ts +70 -0
  51. package/dist/memory/inference-server.d.ts.map +1 -0
  52. package/dist/memory/inference-server.js +395 -0
  53. package/dist/memory/inference-server.js.map +1 -0
  54. package/dist/memory/inference-service-main.d.ts +31 -0
  55. package/dist/memory/inference-service-main.d.ts.map +1 -0
  56. package/dist/memory/inference-service-main.js +89 -0
  57. package/dist/memory/inference-service-main.js.map +1 -0
  58. package/dist/memory/ledger.d.ts +15 -0
  59. package/dist/memory/ledger.d.ts.map +1 -1
  60. package/dist/memory/ledger.js +35 -2
  61. package/dist/memory/ledger.js.map +1 -1
  62. package/dist/memory/model-cache-hygiene.d.ts +10 -0
  63. package/dist/memory/model-cache-hygiene.d.ts.map +1 -1
  64. package/dist/memory/model-cache-hygiene.js +15 -0
  65. package/dist/memory/model-cache-hygiene.js.map +1 -1
  66. package/dist/memory/preference-promotion.d.ts +0 -14
  67. package/dist/memory/preference-promotion.d.ts.map +1 -1
  68. package/dist/memory/preference-promotion.js +17 -0
  69. package/dist/memory/preference-promotion.js.map +1 -1
  70. package/dist/memory/recall-pipeline.d.ts +85 -0
  71. package/dist/memory/recall-pipeline.d.ts.map +1 -0
  72. package/dist/memory/recall-pipeline.js +678 -0
  73. package/dist/memory/recall-pipeline.js.map +1 -0
  74. package/dist/memory/retention-sweep.d.ts +59 -0
  75. package/dist/memory/retention-sweep.d.ts.map +1 -0
  76. package/dist/memory/retention-sweep.js +103 -0
  77. package/dist/memory/retention-sweep.js.map +1 -0
  78. package/dist/memory/store.d.ts +17 -2
  79. package/dist/memory/store.d.ts.map +1 -1
  80. package/dist/memory/store.js +18 -2
  81. package/dist/memory/store.js.map +1 -1
  82. package/dist/memory/vector-channel.d.ts +120 -0
  83. package/dist/memory/vector-channel.d.ts.map +1 -0
  84. package/dist/memory/vector-channel.js +495 -0
  85. package/dist/memory/vector-channel.js.map +1 -0
  86. package/dist/memory/vector-projection.d.ts +87 -0
  87. package/dist/memory/vector-projection.d.ts.map +1 -0
  88. package/dist/memory/vector-projection.js +215 -0
  89. package/dist/memory/vector-projection.js.map +1 -0
  90. package/dist/memory/write-face.d.ts +128 -0
  91. package/dist/memory/write-face.d.ts.map +1 -0
  92. package/dist/memory/write-face.js +504 -0
  93. package/dist/memory/write-face.js.map +1 -0
  94. package/dist/testing/memory-testkit.d.ts +2 -0
  95. package/dist/testing/memory-testkit.d.ts.map +1 -1
  96. package/dist/testing/memory-testkit.js +1 -0
  97. package/dist/testing/memory-testkit.js.map +1 -1
  98. package/package.json +7 -3
  99. package/plugin.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"embedding-reranker.d.ts","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC7C,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC5D;AAED,MAAM,MAAM,4BAA4B,GAAG,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,6BAA6B,CAAC,CAAC;AAsC5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,gBAAgB,CA+CnB","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow lastError;\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"embedding-reranker.d.ts","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+FAA+F;IAC/F,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,MAAM,WAAW,6BAA6B;IAC7C,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC5D;AAED,MAAM,MAAM,4BAA4B,GAAG,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,KACd,OAAO,CAAC,6BAA6B,CAAC,CAAC;AAsC5C;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACjD,iGAAiG;IACjG,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,gBAAgB,CA0DnB","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { join } from "node:path";
26
26
  import { importHostOrBareModule } from "./host-module-import.js";
27
- import { purgeZeroByteCacheArtifacts } from "./model-cache-hygiene.js";
27
+ import { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from "./model-cache-hygiene.js";
28
28
  const DEFAULT_RERANKER_MODEL_ID = "Xenova/bge-reranker-base";
29
29
  const DEFAULT_REMOTE_HOST = "https://huggingface.co";
30
30
  const createDefaultTransformersLoadImpl = (moduleEntryPath) => {
@@ -80,10 +80,22 @@ export function createTransformersReranker(options) {
80
80
  // 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。
81
81
  purgeZeroByteCacheArtifacts(join(cacheDir, modelId));
82
82
  }
83
+ // 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断
84
+ // (fetch 网络错/5xx)。对"仓库 API 清单缺文件"型不生效——该结果被
85
+ // transformers 模块级 memoize (见 annotateUpstreamListingPoison),
86
+ // 进程内换 host 也拿同一空清单。
87
+ try {
88
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
89
+ return await load(modelId, cacheDir, remoteHost);
90
+ }
91
+ catch (error) {
92
+ lastError = error;
93
+ purgeZeroByteCacheArtifacts(join(cacheDir, modelId));
94
+ }
83
95
  }
84
96
  if (lastError === undefined)
85
97
  throw new Error(`no remote hosts configured for model ${modelId}`);
86
- throw lastError;
98
+ throw annotateUpstreamListingPoison(lastError);
87
99
  })();
88
100
  backendPromise = attempt;
89
101
  attempt.catch(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"embedding-reranker.js","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow lastError;\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"embedding-reranker.js","sourceRoot":"","sources":["../../src/memory/embedding-reranker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory cross-encoder reranker for hybrid retrieval (混合检索 · 精排侧).\n *\n * POC-verified (2026-09, 100k corpus): reranking drift-query retrieval with\n * Xenova/bge-reranker-base lifts high-tier recall to re@10 100% (低档 83%),\n * inference deterministic across runs. Reranking is an independently\n * disable-able component (宪法: 独立可禁用); tiers are caller configuration.\n *\n * Model note: default \"Xenova/bge-reranker-base\" is the only officially\n * converted (transformers.js) reranker verified in POC; q8 weights ~280MB,\n * 512-token context — the caller owns the write-side length contract\n * (statement ≤700 chars); overlong inputs are gracefully truncated by the\n * tokenizer (`truncation: true`). Upgrade path bge-reranker-v2-m3 is a\n * modelId swap away, but it currently has no official transformers.js\n * conversion.\n *\n * Runtime dep @huggingface/transformers is lazy-loaded via `await import` at\n * first rerank (惯例参考 packages/agent-forge/src/utils/photon.ts); type\n * positions use type-only imports. Any failure propagates — 不静默降级.\n */\n\n/**\n * Cross-encoder reranker. Higher score = more relevant; raw logits are only\n * order-preserving, not calibrated probabilities.\n */\nimport { join } from \"node:path\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\nimport { annotateUpstreamListingPoison, purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\nexport interface MemoryRerankerV1 {\n\treadonly modelId: string;\n\t/** Score each doc against the query; higher = more relevant (raw logits, order-preserving). */\n\trerank(query: string, docs: readonly string[]): Promise<readonly number[]>;\n}\n\n/**\n * Minimal structural seam around a transformers.js sequence-classification\n * pair, so tests can inject a fake without touching the real loader.\n */\nexport interface TransformersRerankerBackendV1 {\n\t/** Score docs against the query; result[i] corresponds to docs[i]. */\n\tscore: (query: string, docs: string[]) => Promise<number[]>;\n}\n\nexport type TransformersRerankerLoadImpl = (\n\tmodelId: string,\n\tcacheDir: string,\n\tremoteHost: string,\n) => Promise<TransformersRerankerBackendV1>;\n\nconst DEFAULT_RERANKER_MODEL_ID = \"Xenova/bge-reranker-base\";\nconst DEFAULT_REMOTE_HOST = \"https://huggingface.co\";\n\n/** Structural view of the tokenizer call; returns the model-input object. */\ntype RerankTokenizerCall = (\n\ttexts: string[],\n\toptions: { text_pair: string[]; padding: boolean; truncation: boolean },\n) => unknown;\n\n/** Structural view of the sequence-classification forward pass. */\ntype RerankModelCall = (inputs: unknown) => Promise<{ logits: { data: ArrayLike<number> } }>;\n\nconst createDefaultTransformersLoadImpl = (moduleEntryPath?: string): TransformersRerankerLoadImpl => {\n\treturn async (modelId, cacheDir, remoteHost) => {\n\t\tconst transformers = await importHostOrBareModule<typeof import(\"@huggingface/transformers\")>(\n\t\t\tmoduleEntryPath ?? \"@huggingface/transformers\",\n\t\t);\n\t\ttransformers.env.cacheDir = cacheDir;\n\t\ttransformers.env.remoteHost = remoteHost;\n\t\tconst tokenizer = (await transformers.AutoTokenizer.from_pretrained(modelId)) as unknown as RerankTokenizerCall;\n\t\tconst model = (await transformers.AutoModelForSequenceClassification.from_pretrained(modelId, {\n\t\t\tdtype: \"q8\",\n\t\t})) as unknown as RerankModelCall;\n\t\treturn {\n\t\t\tscore: async (query, docs) => {\n\t\t\t\tconst inputs = tokenizer(\n\t\t\t\t\tdocs.map(() => query),\n\t\t\t\t\t{ text_pair: docs, padding: true, truncation: true },\n\t\t\t\t);\n\t\t\t\tconst output = await model(inputs);\n\t\t\t\treturn Array.from(output.logits.data);\n\t\t\t},\n\t\t};\n\t};\n};\n\n/**\n * Transformers.js-backed cross-encoder reranker (两档共用默认\n * Xenova/bge-reranker-base, 见文件头模型说明)。\n *\n * 镜像序列: 首次 rerank 时按 `remoteHosts` 顺序逐个尝试加载 (调用方通常传\n * [\"https://huggingface.co\", \"https://hf-mirror.com\"]); 某 host 加载抛错则\n * 尝试下一个, 全部失败时抛最后一个错误。host 确定后模型句柄只加载一次\n * (并发 rerank 共享同一 in-flight promise); 加载失败不缓存, 下次 rerank\n * 重新走完整序列, 失败原因原样保留, 不静默降级。\n *\n * `loadImpl` 是测试 seam: 注入后绕过真实加载, 签名\n * (modelId, cacheDir, remoteHost) => backend。\n */\nexport function createTransformersReranker(options: {\n\treadonly modelId?: string;\n\treadonly cacheDir: string;\n\treadonly remoteHosts?: readonly string[];\n\treadonly loadImpl?: TransformersRerankerLoadImpl;\n\t/** Host-resolved transformers entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): MemoryRerankerV1 {\n\tconst modelId = options.modelId ?? DEFAULT_RERANKER_MODEL_ID;\n\tconst cacheDir = options.cacheDir;\n\tconst remoteHosts = options.remoteHosts ?? [DEFAULT_REMOTE_HOST];\n\tconst load = options.loadImpl ?? createDefaultTransformersLoadImpl(options.moduleEntryPath);\n\n\tlet backendPromise: Promise<TransformersRerankerBackendV1> | null = null;\n\tconst loadBackend = (): Promise<TransformersRerankerBackendV1> => {\n\t\tconst existing = backendPromise;\n\t\tif (existing) {\n\t\t\treturn existing;\n\t\t}\n\t\tconst attempt = (async () => {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const remoteHost of remoteHosts) {\n\t\t\t\ttry {\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\t// 镜像轮换前清 0 字节残骸 (embedding-provider 同理, model-cache-hygiene)。\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t\t// 同 host 一次短退避重试 (2026-10-06 live E2E): 覆盖 rejection 型瞬断\n\t\t\t\t// (fetch 网络错/5xx)。对\"仓库 API 清单缺文件\"型不生效——该结果被\n\t\t\t\t// transformers 模块级 memoize (见 annotateUpstreamListingPoison),\n\t\t\t\t// 进程内换 host 也拿同一空清单。\n\t\t\t\ttry {\n\t\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 2_000));\n\t\t\t\t\treturn await load(modelId, cacheDir, remoteHost);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlastError = error;\n\t\t\t\t\tpurgeZeroByteCacheArtifacts(join(cacheDir, modelId));\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (lastError === undefined) throw new Error(`no remote hosts configured for model ${modelId}`);\n\t\t\tthrow annotateUpstreamListingPoison(lastError);\n\t\t})();\n\t\tbackendPromise = attempt;\n\t\tattempt.catch(() => {\n\t\t\t// 失败不缓存: 下次 rerank 重新走镜像序列 (显式失败, 不静默降级)。\n\t\t\tif (backendPromise === attempt) {\n\t\t\t\tbackendPromise = null;\n\t\t\t}\n\t\t});\n\t\treturn attempt;\n\t};\n\n\treturn {\n\t\tmodelId,\n\t\trerank: async (query, docs) => {\n\t\t\tif (docs.length === 0) {\n\t\t\t\t// 空 docs 直接返回, 不触发模型加载 (lazy-load 只发生在真正需要打分时)。\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tconst backend = await loadBackend();\n\t\t\treturn backend.score(query, [...docs]);\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * enabling 前置检查、档位预设与镜像源(capability 拆分战役 slice 1):纯函数,
3
+ * 无闭包依赖。
4
+ */
5
+ import type { MemoryModeV1 } from "@agent-forge/plugin-sdk";
6
+ /** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */
7
+ export declare const DISK_HEADROOM_MIN_BYTES: Readonly<Record<Exclude<MemoryModeV1, "off">, number>>;
8
+ /**
9
+ * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding
10
+ * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →
11
+ * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。
12
+ */
13
+ export declare const MODEL_MIN_WEIGHT_BYTES: Readonly<Record<Exclude<MemoryModeV1, "off">, number>>;
14
+ export declare const RERANKER_MIN_WEIGHT_BYTES: number;
15
+ /** 两档预设 (实施计划 §2): light = 纯中文强项低配档, full = 跨语言大规模默认档。 */
16
+ interface MemoryVectorPresetV1 {
17
+ readonly embeddingModelId: string;
18
+ readonly dimensions: number;
19
+ /** 查询侧指令前缀 (bge-small-zh 系查询侧指令由调用方负责添加; 写入侧不加)。 */
20
+ readonly queryPrefix: string;
21
+ }
22
+ export declare const VECTOR_PRESET_LIGHT: MemoryVectorPresetV1;
23
+ export declare const VECTOR_PRESET_FULL: MemoryVectorPresetV1;
24
+ /** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */
25
+ export declare function vectorPresetForMode(mode: Exclude<MemoryModeV1, "off">): MemoryVectorPresetV1;
26
+ /**
27
+ * agentDir 身份归一化 (2026-10-06 串线修复): 绝对路径解析抹平分隔符与相对段,
28
+ * win32 再 casefold —— 同一目录在 Windows 下的大小写差异 (盘符/路径段) 不得
29
+ * 误判为跨 agent-dir 串线。仅用于身份比对, 不触碰文件系统语义。
30
+ */
31
+ export declare function normalizeMemoryAgentDirIdentity(agentDir: string): string;
32
+ /**
33
+ * 自定义镜像源 (失败行动清单第一项指向它): env `AGENT_FORGE_HF_MIRRORS` 逗号
34
+ * 分隔 host 列表, 排在默认镜像序列之前优先尝试; 默认序列直连 huggingface.co
35
+ * 超时时回落 hf-mirror.com。
36
+ *
37
+ * env 条目归一化 (2026-10-06 live E2E 修复): 裸主机名补 `https://` — 用户
38
+ * 按行动清单写 `AGENT_FORGE_HF_MIRRORS=hf-mirror.com` 时, 无 scheme 的
39
+ * remoteHost 会让 transformers 的文件请求按相对 URL 解析并在模块内留下
40
+ * 毒化状态, 连后续带 scheme 的镜像重试也崩; 行动清单推荐的自救手段本身
41
+ * 不能是断的。已带 scheme 的条目原样保留, 尾部 `/` 去除。
42
+ */
43
+ export declare function remoteHostsForEnabling(): readonly string[];
44
+ export declare function nativeBinaryCheck(hostEntryPath?: string): string | undefined;
45
+ /**
46
+ * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与
47
+ * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的
48
+ * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。
49
+ */
50
+ export declare function resolveHeavyDependencyEntryPath(specifier: string): string;
51
+ /**
52
+ * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与
53
+ * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用
54
+ * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。
55
+ * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧
56
+ * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步
57
+ * 的 "Cannot find module" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。
58
+ */
59
+ export declare function missingHeavyDependencyModules(entries: {
60
+ lancedb?: string;
61
+ transformers?: string;
62
+ }, resolveModule: (specifier: string) => string): string[];
63
+ /**
64
+ * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):
65
+ * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。
66
+ * 返回 undefined = 检查通过; "missing" = 目录/文件缺失或仅剩 0 字节残骸
67
+ * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =
68
+ * 文件存在但可疑 (截断/非空过小), 属失败。
69
+ */
70
+ export declare function modelFilesCheck(cacheDir: string, modelId: string, minWeightBytes: number): string | undefined;
71
+ /**
72
+ * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。
73
+ * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。agentDir 可能尚不存在 (全新
74
+ * 机器首启, 下载前无人创建过它 — 2026-10-06 live E2E 实测踩中) — statfs
75
+ * ENOENT 时逐级上溯最近存在的祖先目录, 同卷即同余量; 上溯到根仍失败才按
76
+ * 无法判定报告。
77
+ */
78
+ export declare function diskHeadroomCheck(agentDir: string, minimumBytes: number): Promise<string | undefined>;
79
+ /** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */
80
+ export declare function enablingActionList(modelCacheDir: string, currentMode: MemoryModeV1): readonly string[];
81
+ /** agent-dir 配置文件里的征询拒绝标记值 (memoryConsent 键)。 */
82
+ export declare const MEMORY_CONSENT_DECLINED_VALUE = "declined";
83
+ /** memory 插件 agent-dir 配置通道 (entry 读、capability 写, 同一路径)。 */
84
+ export declare const MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH: string;
85
+ /** agent-dir 配置里用户显式持久化的档位键 (无 / 该键即 undefined)。 */
86
+ export declare function readMemorySettingsMode(agentDir: string): "light" | "full" | undefined;
87
+ /**
88
+ * 拒绝/跳过的持久化 (2026-10-05 用户裁决): 写入插件自己的 agent-dir 配置文件,
89
+ * 不改写用户 settings.json。语义是「用户对默认档说了不」——仅抑制默认档;
90
+ * 用户日后显式设置 settings.json `memory.mode` (light/full) 时显式值仍然生效。
91
+ * 同时移除 /memory light|full 持久化的档位键, 保证"先升 full 再 off"不会留下
92
+ * 一个压过 settings 的陈旧档位。
93
+ */
94
+ export declare function writeMemoryConsentDeclined(agentDir: string): void;
95
+ /**
96
+ * 档位升级/降级的持久化 (/memory light|full): 写入插件 agent-dir 配置的
97
+ * `memorySettingsMode` 键——与 settings.json `memory.mode` 同一解析层 (持久
98
+ * 配置面, env 仍可临时覆盖)。插件无 settings.json 写通道 (HostCapabilities
99
+ * 无该面), 本文件键在其上, 即命令是比 settings 更近一次的显式表达。同时清
100
+ * 除拒绝标记: 显式升/降档是最新的用户意图, 压过此前的"对默认档说不"。
101
+ */
102
+ export declare function writeMemorySettingsMode(agentDir: string, mode: "light" | "full"): void;
103
+ export {};
104
+ //# sourceMappingURL=enabling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enabling.d.ts","sourceRoot":"","sources":["../../src/memory/enabling.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,4FAA+D;AAC/D,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAG1F,CAAC;AACF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAGzF,CAAC;AACF,eAAO,MAAM,yBAAyB,QAAmB,CAAC;AAI1D,+GAA0D;AAC1D,UAAU,oBAAoB;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0GAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,mBAAmB,EAAE,oBAIjC,CAAC;AACF,eAAO,MAAM,kBAAkB,EAAE,oBAIhC,CAAC;AAIF,wFAA0C;AAC1C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,GAAG,oBAAoB,CAE5F;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAO1D;AAsBD,wBAAgB,iBAAiB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkC5E;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CASzE;AAED;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EACpD,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAC1C,MAAM,EAAE,CAeV;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoB7G;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAyB3G;AAED,6EAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,GAAG,SAAS,MAAM,EAAE,CAUtG;AAQD,+EAAiD;AACjD,eAAO,MAAM,6BAA6B,aAAa,CAAC;AACxD,yFAA6D;AAC7D,eAAO,MAAM,qCAAqC,QAA8C,CAAC;AAgCjG,0FAAoD;AACpD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAGrF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMjE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKtF","sourcesContent":["/**\n * enabling 前置检查、档位预设与镜像源(capability 拆分战役 slice 1):纯函数,\n * 无闭包依赖。\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from \"node:fs\";\nimport { statfs } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { MemoryModeV1 } from \"@agent-forge/plugin-sdk\";\nimport { purgeZeroByteCacheArtifacts } from \"./model-cache-hygiene.ts\";\n\n/** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */\nexport const DISK_HEADROOM_MIN_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 500 * 1024 * 1024,\n\tfull: 2 * 1024 * 1024 * 1024,\n};\n/**\n * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding\n * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →\n * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。\n */\nexport const MODEL_MIN_WEIGHT_BYTES: Readonly<Record<Exclude<MemoryModeV1, \"off\">, number>> = {\n\tlight: 8 * 1024 * 1024,\n\tfull: 256 * 1024 * 1024,\n};\nexport const RERANKER_MIN_WEIGHT_BYTES = 64 * 1024 * 1024;\n/** 自定义镜像源环境变量 (失败行动清单第一项指向它)。 */\nconst HF_MIRRORS_ENV = \"AGENT_FORGE_HF_MIRRORS\";\n\n/** 两档预设 (实施计划 §2): light = 纯中文强项低配档, full = 跨语言大规模默认档。 */\ninterface MemoryVectorPresetV1 {\n\treadonly embeddingModelId: string;\n\treadonly dimensions: number;\n\t/** 查询侧指令前缀 (bge-small-zh 系查询侧指令由调用方负责添加; 写入侧不加)。 */\n\treadonly queryPrefix: string;\n}\n\nexport const VECTOR_PRESET_LIGHT: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-small-zh-v1.5\",\n\tdimensions: 512,\n\tqueryPrefix: \"为这个句子生成表示以用于检索相关文章:\",\n};\nexport const VECTOR_PRESET_FULL: MemoryVectorPresetV1 = {\n\tembeddingModelId: \"Xenova/bge-m3\",\n\tdimensions: 1024,\n\tqueryPrefix: \"\",\n};\n/** 镜像序列 (实施计划 §7): 直连 huggingface.co 超时时回落 hf-mirror.com。 */\nconst VECTOR_REMOTE_HOSTS: readonly string[] = [\"https://huggingface.co\", \"https://hf-mirror.com\"];\n\n/** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */\nexport function vectorPresetForMode(mode: Exclude<MemoryModeV1, \"off\">): MemoryVectorPresetV1 {\n\treturn mode === \"light\" ? VECTOR_PRESET_LIGHT : VECTOR_PRESET_FULL;\n}\n\n/**\n * agentDir 身份归一化 (2026-10-06 串线修复): 绝对路径解析抹平分隔符与相对段,\n * win32 再 casefold —— 同一目录在 Windows 下的大小写差异 (盘符/路径段) 不得\n * 误判为跨 agent-dir 串线。仅用于身份比对, 不触碰文件系统语义。\n */\nexport function normalizeMemoryAgentDirIdentity(agentDir: string): string {\n\tconst resolvedPath = resolve(agentDir);\n\treturn process.platform === \"win32\" ? resolvedPath.toLowerCase() : resolvedPath;\n}\n\n/**\n * 自定义镜像源 (失败行动清单第一项指向它): env `AGENT_FORGE_HF_MIRRORS` 逗号\n * 分隔 host 列表, 排在默认镜像序列之前优先尝试; 默认序列直连 huggingface.co\n * 超时时回落 hf-mirror.com。\n *\n * env 条目归一化 (2026-10-06 live E2E 修复): 裸主机名补 `https://` — 用户\n * 按行动清单写 `AGENT_FORGE_HF_MIRRORS=hf-mirror.com` 时, 无 scheme 的\n * remoteHost 会让 transformers 的文件请求按相对 URL 解析并在模块内留下\n * 毒化状态, 连后续带 scheme 的镜像重试也崩; 行动清单推荐的自救手段本身\n * 不能是断的。已带 scheme 的条目原样保留, 尾部 `/` 去除。\n */\nexport function remoteHostsForEnabling(): readonly string[] {\n\tconst custom = process.env[HF_MIRRORS_ENV]\n\t\t?.split(\",\")\n\t\t.map((host) => host.trim().replace(/\\/+$/, \"\"))\n\t\t.map((host) => (host === \"\" || host.includes(\"://\") ? host : `https://${host}`))\n\t\t.filter((host) => host !== \"\");\n\treturn custom === undefined || custom.length === 0 ? VECTOR_REMOTE_HOSTS : [...custom, ...VECTOR_REMOTE_HOSTS];\n}\n\n/**\n * 本地检查 1: native .node sidecar 可解析 (设计 §3.2 checking)。布局:\n * onnxruntime-node/bin 下的 napi-vN/<platform>/<arch> 原生模块 — 缺失 = 安装\n * 损坏。容错: 不钉死 napi 版本号, 逐目录扫描。\n *\n * 解析基点双通道: jiti(安装店加载缝)注入的 `require` 走宿主 alias 表\n * (宿主 node_modules 副本, 店内无依赖树); 纯 ESM(源码/工作区直载)下裸\n * `require` 未定义(typeof 探测不抛 ReferenceError), 回落原生 createRequire。\n */\nfunction resolveOnnxRuntimeEntryPath(): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(\"onnxruntime-node\");\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析再报结构化错误。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(\"onnxruntime-node\");\n}\n\nexport function nativeBinaryCheck(hostEntryPath?: string): string | undefined {\n\t// 两种失败要区分 (D-075 安装店路径修复后前者成为真实可达路径):\n\t// (a) 宿主未注入入口路径且插件位置自解析不到 — 不是安装损坏, 行动是宿主侧\n\t// 安装重依赖后重启会话, 不是重装插件;\n\t// (b) 路径存在但原生二进制缺失/损坏 — 保留损坏类文案。\n\tlet entryPath: string;\n\tif (hostEntryPath === undefined) {\n\t\ttry {\n\t\t\tentryPath = resolveOnnxRuntimeEntryPath();\n\t\t} catch (error) {\n\t\t\tconst detail = error instanceof Error ? error.message : String(error);\n\t\t\treturn `onnxruntime-node is not provided by the host installation and could not be resolved from the plugin location (${detail}); to enable memory features, install onnxruntime-node in the host project (npm i onnxruntime-node) and restart the session`;\n\t\t}\n\t} else {\n\t\tentryPath = hostEntryPath;\n\t}\n\ttry {\n\t\tlet packageDir = dirname(entryPath);\n\t\twhile (packageDir !== dirname(packageDir) && !existsSync(join(packageDir, \"package.json\"))) {\n\t\t\tpackageDir = dirname(packageDir);\n\t\t}\n\t\tconst binDir = join(packageDir, \"bin\");\n\t\tif (!existsSync(binDir)) return `onnxruntime native binary directory not found: ${binDir}`;\n\t\tfor (const napiEntry of readdirSync(binDir, { withFileTypes: true })) {\n\t\t\tif (!napiEntry.isDirectory() || !napiEntry.name.startsWith(\"napi-v\")) continue;\n\t\t\tconst platformDir = join(binDir, napiEntry.name, process.platform, process.arch);\n\t\t\tif (!existsSync(platformDir)) continue;\n\t\t\tconst binaries = readdirSync(platformDir).filter((file) => file.endsWith(\".node\"));\n\t\t\tif (binaries.length > 0) return undefined;\n\t\t}\n\t\treturn `onnxruntime native binary for ${process.platform}/${process.arch} not found under ${binDir}`;\n\t} catch (error) {\n\t\treturn `onnxruntime native binary could not be resolved (${error instanceof Error ? error.message : String(error)}); the installation looks damaged — reinstall the package`;\n\t}\n}\n\n/**\n * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与\n * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的\n * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。\n */\nexport function resolveHeavyDependencyEntryPath(specifier: string): string {\n\tif (typeof require === \"function\") {\n\t\ttry {\n\t\t\treturn require.resolve(specifier);\n\t\t} catch {\n\t\t\t// jiti 解析失败(alias 未覆盖等)→ 继续走原生解析。\n\t\t}\n\t}\n\treturn createRequire(import.meta.url).resolve(specifier);\n}\n\n/**\n * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与\n * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用\n * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。\n * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧\n * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步\n * 的 \"Cannot find module\" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。\n */\nexport function missingHeavyDependencyModules(\n\tentries: { lancedb?: string; transformers?: string },\n\tresolveModule: (specifier: string) => string,\n): string[] {\n\tconst missing: string[] = [];\n\tconst supplies: readonly (readonly [specifier: string, entryPath: string | undefined])[] = [\n\t\t[\"@lancedb/lancedb\", entries.lancedb],\n\t\t[\"@huggingface/transformers\", entries.transformers],\n\t];\n\tfor (const [specifier, entryPath] of supplies) {\n\t\tif (entryPath !== undefined) continue;\n\t\ttry {\n\t\t\tresolveModule(specifier);\n\t\t} catch {\n\t\t\tmissing.push(specifier);\n\t\t}\n\t}\n\treturn missing;\n}\n\n/**\n * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):\n * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。\n * 返回 undefined = 检查通过; \"missing\" = 目录/文件缺失或仅剩 0 字节残骸\n * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =\n * 文件存在但可疑 (截断/非空过小), 属失败。\n */\nexport function modelFilesCheck(cacheDir: string, modelId: string, minWeightBytes: number): string | undefined {\n\tconst modelDir = join(cacheDir, modelId);\n\tif (!existsSync(modelDir)) return \"missing\";\n\t// tokenizer 只剩 0 字节文件 = 失败下载残骸 (真 tokenizer.json 不可能为空):\n\t// 清掉按缺失处理可重下; 非空文件不动 (手动导入布局不受影响)。\n\tconst tokenizerNames = [\"tokenizer.json\", \"tokenizer.model\"].filter((name) => existsSync(join(modelDir, name)));\n\tconst hasTokenizer = tokenizerNames.some((name) => statSync(join(modelDir, name)).size > 0);\n\tif (!hasTokenizer) {\n\t\tif (tokenizerNames.length > 0) purgeZeroByteCacheArtifacts(modelDir);\n\t\treturn \"missing\";\n\t}\n\tconst onnxDir = join(modelDir, \"onnx\");\n\tif (!existsSync(onnxDir)) return `onnx weight directory missing under ${modelDir}`;\n\tconst weights = readdirSync(onnxDir).filter((name) => name.includes(\".onnx\"));\n\tif (weights.length === 0) return `no onnx weight file under ${onnxDir}`;\n\tconst largest = Math.max(...weights.map((name) => statSync(join(onnxDir, name)).size));\n\tif (largest < minWeightBytes) {\n\t\treturn `largest onnx weight under ${onnxDir} is ${largest} bytes, below the expected minimum ${minWeightBytes} — the download looks truncated`;\n\t}\n\treturn undefined;\n}\n\n/**\n * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。\n * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。agentDir 可能尚不存在 (全新\n * 机器首启, 下载前无人创建过它 — 2026-10-06 live E2E 实测踩中) — statfs\n * ENOENT 时逐级上溯最近存在的祖先目录, 同卷即同余量; 上溯到根仍失败才按\n * 无法判定报告。\n */\nexport async function diskHeadroomCheck(agentDir: string, minimumBytes: number): Promise<string | undefined> {\n\tlet freeBytes: number | undefined;\n\tlet probe: string | undefined = agentDir;\n\tfor (let attempt = 0; probe !== undefined && attempt < 16; attempt++) {\n\t\ttry {\n\t\t\tconst stats = await statfs(probe);\n\t\t\tfreeBytes = stats.bfree * stats.bsize;\n\t\t\tbreak;\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof Error) || !String(error.message).includes(\"ENOENT\")) {\n\t\t\t\treturn `disk free space could not be determined for ${agentDir} (probing ${probe}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t})`;\n\t\t\t}\n\t\t\tconst parent = dirname(probe);\n\t\t\tprobe = parent === probe ? undefined : parent;\n\t\t}\n\t}\n\tif (freeBytes === undefined) {\n\t\treturn `disk free space could not be determined for ${agentDir} (no existing ancestor directory to statfs)`;\n\t}\n\tif (freeBytes < minimumBytes) {\n\t\treturn `disk free space is ${freeBytes} bytes, below the required ${minimumBytes}`;\n\t}\n\treturn undefined;\n}\n\n/** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */\nexport function enablingActionList(modelCacheDir: string, currentMode: MemoryModeV1): readonly string[] {\n\treturn [\n\t\t`Switch to a reachable mirror: set ${HF_MIRRORS_ENV} to a comma-separated host list (tried before the defaults)`,\n\t\t`Install the heavy dependencies in the host project (npm i onnxruntime-node @lancedb/lancedb @huggingface/transformers) so the host can supply them via hostDependencyPaths, then restart the session`,\n\t\t`Download the models manually and place them under ${modelCacheDir} (layout: <modelId>/tokenizer.json + <modelId>/onnx/model*.onnx)`,\n\t\tcurrentMode === \"full\"\n\t\t\t? 'Downgrade to memory.mode \"light\" (much smaller models)'\n\t\t\t: 'Keep memory.mode \"light\" and retry with a reachable mirror',\n\t\t'Give up: leave memory.mode \"off\" (the default) so the memory family stays unregistered',\n\t];\n}\n\n// ---------------------------------------------------------------------------\n// 征询持久化 (capability 拆分战役 slice 3b 随迁, 征询域): agent-dir 配置文件\n// 的读写与征询拒绝/档位落盘。纯函数, 路径域显式传入; capability.ts 经\n// re-export 维持历史导出面, 向量通道 (vector-channel.ts) 直接 import。\n// ---------------------------------------------------------------------------\n\n/** agent-dir 配置文件里的征询拒绝标记值 (memoryConsent 键)。 */\nexport const MEMORY_CONSENT_DECLINED_VALUE = \"declined\";\n/** memory 插件 agent-dir 配置通道 (entry 读、capability 写, 同一路径)。 */\nexport const MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH = join(\"capabilities\", \"builtin.memory.json\");\n\nfunction readMemoryAgentDirConfig(agentDir: string): Record<string, unknown> {\n\tconst configPath = join(agentDir, MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH);\n\tif (existsSync(configPath)) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(configPath, \"utf8\"));\n\t\t\tif (parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\t\treturn parsed as Record<string, unknown>;\n\t\t\t}\n\t\t} catch {\n\t\t\t// 既有文件非法时以本次写入为准 (持久化是显式用户动作, 不被脏文件阻塞)。\n\t\t}\n\t}\n\treturn {};\n}\n\n/**\n * 写 agent-dir 配置的原子替换 (2026-10-06 复审 P1-2): temp 文件 + rename —\n * 自动升档引入了无人值守写者, 与其它进程的 /memory off (会删档位键) 读改写\n * 交错时, 直接 writeFileSync 可能留下半截 JSON 让并发读者拿到空配置 (整块\n * 丢键: 抹掉并发 decline 或升档)。rename 在同一目录内原子替换, 读者要么见\n * 旧文件要么见新文件。\n */\nfunction writeMemoryAgentDirConfig(agentDir: string, config: Record<string, unknown>): void {\n\tconst configPath = join(agentDir, MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH);\n\tmkdirSync(dirname(configPath), { recursive: true });\n\tconst tempPath = `${configPath}.${process.pid}.tmp`;\n\twriteFileSync(tempPath, `${JSON.stringify(config, null, \"\\t\")}\\n`, \"utf8\");\n\trenameSync(tempPath, configPath);\n}\n\n/** agent-dir 配置里用户显式持久化的档位键 (无 / 该键即 undefined)。 */\nexport function readMemorySettingsMode(agentDir: string): \"light\" | \"full\" | undefined {\n\tconst value = readMemoryAgentDirConfig(agentDir).memorySettingsMode;\n\treturn value === \"light\" || value === \"full\" ? value : undefined;\n}\n\n/**\n * 拒绝/跳过的持久化 (2026-10-05 用户裁决): 写入插件自己的 agent-dir 配置文件,\n * 不改写用户 settings.json。语义是「用户对默认档说了不」——仅抑制默认档;\n * 用户日后显式设置 settings.json `memory.mode` (light/full) 时显式值仍然生效。\n * 同时移除 /memory light|full 持久化的档位键, 保证\"先升 full 再 off\"不会留下\n * 一个压过 settings 的陈旧档位。\n */\nexport function writeMemoryConsentDeclined(agentDir: string): void {\n\tconst config = readMemoryAgentDirConfig(agentDir);\n\tconfig.memoryConsent = MEMORY_CONSENT_DECLINED_VALUE;\n\tdelete config.memorySettingsMode;\n\tdelete config.memoryMode;\n\twriteMemoryAgentDirConfig(agentDir, config);\n}\n\n/**\n * 档位升级/降级的持久化 (/memory light|full): 写入插件 agent-dir 配置的\n * `memorySettingsMode` 键——与 settings.json `memory.mode` 同一解析层 (持久\n * 配置面, env 仍可临时覆盖)。插件无 settings.json 写通道 (HostCapabilities\n * 无该面), 本文件键在其上, 即命令是比 settings 更近一次的显式表达。同时清\n * 除拒绝标记: 显式升/降档是最新的用户意图, 压过此前的\"对默认档说不\"。\n */\nexport function writeMemorySettingsMode(agentDir: string, mode: \"light\" | \"full\"): void {\n\tconst config = readMemoryAgentDirConfig(agentDir);\n\tconfig.memorySettingsMode = mode;\n\tdelete config.memoryConsent;\n\twriteMemoryAgentDirConfig(agentDir, config);\n}\n"]}
@@ -0,0 +1,321 @@
1
+ /**
2
+ * enabling 前置检查、档位预设与镜像源(capability 拆分战役 slice 1):纯函数,
3
+ * 无闭包依赖。
4
+ */
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
6
+ import { statfs } from "node:fs/promises";
7
+ import { createRequire } from "node:module";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { purgeZeroByteCacheArtifacts } from "./model-cache-hygiene.js";
10
+ /** 开启前磁盘余量下限 (设计 §3.2 checking): light ≥ 500MB, full ≥ 2GB。 */
11
+ export const DISK_HEADROOM_MIN_BYTES = {
12
+ light: 500 * 1024 * 1024,
13
+ full: 2 * 1024 * 1024 * 1024,
14
+ };
15
+ /**
16
+ * 模型文件大小下限 (防截断/空文件的 sanity 下限, 非精确体积): 高档 embedding
17
+ * (bge-m3 q8) 数百 MB → 下限 256MB, 精排器 (bge-reranker-base q8) ~280MB →
18
+ * 下限 64MB, 低档 (bge-small-zh q8) ~24MB → 下限 8MB。真实校验由加载冒烟承担。
19
+ */
20
+ export const MODEL_MIN_WEIGHT_BYTES = {
21
+ light: 8 * 1024 * 1024,
22
+ full: 256 * 1024 * 1024,
23
+ };
24
+ export const RERANKER_MIN_WEIGHT_BYTES = 64 * 1024 * 1024;
25
+ /** 自定义镜像源环境变量 (失败行动清单第一项指向它)。 */
26
+ const HF_MIRRORS_ENV = "AGENT_FORGE_HF_MIRRORS";
27
+ export const VECTOR_PRESET_LIGHT = {
28
+ embeddingModelId: "Xenova/bge-small-zh-v1.5",
29
+ dimensions: 512,
30
+ queryPrefix: "为这个句子生成表示以用于检索相关文章:",
31
+ };
32
+ export const VECTOR_PRESET_FULL = {
33
+ embeddingModelId: "Xenova/bge-m3",
34
+ dimensions: 1024,
35
+ queryPrefix: "",
36
+ };
37
+ /** 镜像序列 (实施计划 §7): 直连 huggingface.co 超时时回落 hf-mirror.com。 */
38
+ const VECTOR_REMOTE_HOSTS = ["https://huggingface.co", "https://hf-mirror.com"];
39
+ /** 档位预设: mode → 模型组合。off 在进入装配前已被门控拦截。 */
40
+ export function vectorPresetForMode(mode) {
41
+ return mode === "light" ? VECTOR_PRESET_LIGHT : VECTOR_PRESET_FULL;
42
+ }
43
+ /**
44
+ * agentDir 身份归一化 (2026-10-06 串线修复): 绝对路径解析抹平分隔符与相对段,
45
+ * win32 再 casefold —— 同一目录在 Windows 下的大小写差异 (盘符/路径段) 不得
46
+ * 误判为跨 agent-dir 串线。仅用于身份比对, 不触碰文件系统语义。
47
+ */
48
+ export function normalizeMemoryAgentDirIdentity(agentDir) {
49
+ const resolvedPath = resolve(agentDir);
50
+ return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
51
+ }
52
+ /**
53
+ * 自定义镜像源 (失败行动清单第一项指向它): env `AGENT_FORGE_HF_MIRRORS` 逗号
54
+ * 分隔 host 列表, 排在默认镜像序列之前优先尝试; 默认序列直连 huggingface.co
55
+ * 超时时回落 hf-mirror.com。
56
+ *
57
+ * env 条目归一化 (2026-10-06 live E2E 修复): 裸主机名补 `https://` — 用户
58
+ * 按行动清单写 `AGENT_FORGE_HF_MIRRORS=hf-mirror.com` 时, 无 scheme 的
59
+ * remoteHost 会让 transformers 的文件请求按相对 URL 解析并在模块内留下
60
+ * 毒化状态, 连后续带 scheme 的镜像重试也崩; 行动清单推荐的自救手段本身
61
+ * 不能是断的。已带 scheme 的条目原样保留, 尾部 `/` 去除。
62
+ */
63
+ export function remoteHostsForEnabling() {
64
+ const custom = process.env[HF_MIRRORS_ENV]
65
+ ?.split(",")
66
+ .map((host) => host.trim().replace(/\/+$/, ""))
67
+ .map((host) => (host === "" || host.includes("://") ? host : `https://${host}`))
68
+ .filter((host) => host !== "");
69
+ return custom === undefined || custom.length === 0 ? VECTOR_REMOTE_HOSTS : [...custom, ...VECTOR_REMOTE_HOSTS];
70
+ }
71
+ /**
72
+ * 本地检查 1: native .node sidecar 可解析 (设计 §3.2 checking)。布局:
73
+ * onnxruntime-node/bin 下的 napi-vN/<platform>/<arch> 原生模块 — 缺失 = 安装
74
+ * 损坏。容错: 不钉死 napi 版本号, 逐目录扫描。
75
+ *
76
+ * 解析基点双通道: jiti(安装店加载缝)注入的 `require` 走宿主 alias 表
77
+ * (宿主 node_modules 副本, 店内无依赖树); 纯 ESM(源码/工作区直载)下裸
78
+ * `require` 未定义(typeof 探测不抛 ReferenceError), 回落原生 createRequire。
79
+ */
80
+ function resolveOnnxRuntimeEntryPath() {
81
+ if (typeof require === "function") {
82
+ try {
83
+ return require.resolve("onnxruntime-node");
84
+ }
85
+ catch {
86
+ // jiti 解析失败(alias 未覆盖等)→ 继续走原生解析再报结构化错误。
87
+ }
88
+ }
89
+ return createRequire(import.meta.url).resolve("onnxruntime-node");
90
+ }
91
+ export function nativeBinaryCheck(hostEntryPath) {
92
+ // 两种失败要区分 (D-075 安装店路径修复后前者成为真实可达路径):
93
+ // (a) 宿主未注入入口路径且插件位置自解析不到 — 不是安装损坏, 行动是宿主侧
94
+ // 安装重依赖后重启会话, 不是重装插件;
95
+ // (b) 路径存在但原生二进制缺失/损坏 — 保留损坏类文案。
96
+ let entryPath;
97
+ if (hostEntryPath === undefined) {
98
+ try {
99
+ entryPath = resolveOnnxRuntimeEntryPath();
100
+ }
101
+ catch (error) {
102
+ const detail = error instanceof Error ? error.message : String(error);
103
+ return `onnxruntime-node is not provided by the host installation and could not be resolved from the plugin location (${detail}); to enable memory features, install onnxruntime-node in the host project (npm i onnxruntime-node) and restart the session`;
104
+ }
105
+ }
106
+ else {
107
+ entryPath = hostEntryPath;
108
+ }
109
+ try {
110
+ let packageDir = dirname(entryPath);
111
+ while (packageDir !== dirname(packageDir) && !existsSync(join(packageDir, "package.json"))) {
112
+ packageDir = dirname(packageDir);
113
+ }
114
+ const binDir = join(packageDir, "bin");
115
+ if (!existsSync(binDir))
116
+ return `onnxruntime native binary directory not found: ${binDir}`;
117
+ for (const napiEntry of readdirSync(binDir, { withFileTypes: true })) {
118
+ if (!napiEntry.isDirectory() || !napiEntry.name.startsWith("napi-v"))
119
+ continue;
120
+ const platformDir = join(binDir, napiEntry.name, process.platform, process.arch);
121
+ if (!existsSync(platformDir))
122
+ continue;
123
+ const binaries = readdirSync(platformDir).filter((file) => file.endsWith(".node"));
124
+ if (binaries.length > 0)
125
+ return undefined;
126
+ }
127
+ return `onnxruntime native binary for ${process.platform}/${process.arch} not found under ${binDir}`;
128
+ }
129
+ catch (error) {
130
+ return `onnxruntime native binary could not be resolved (${error instanceof Error ? error.message : String(error)}); the installation looks damaged — reinstall the package`;
131
+ }
132
+ }
133
+ /**
134
+ * 重依赖自解析默认实现 (missingHeavyDependencyModules 生产接线): 双通道与
135
+ * {@link resolveOnnxRuntimeEntryPath} 一致 — jiti(安装店加载缝)注入的
136
+ * `require` 走宿主 alias 表; 纯 ESM(源码/工作区直载)下回落原生 createRequire。
137
+ */
138
+ export function resolveHeavyDependencyEntryPath(specifier) {
139
+ if (typeof require === "function") {
140
+ try {
141
+ return require.resolve(specifier);
142
+ }
143
+ catch {
144
+ // jiti 解析失败(alias 未覆盖等)→ 继续走原生解析。
145
+ }
146
+ }
147
+ return createRequire(import.meta.url).resolve(specifier);
148
+ }
149
+ /**
150
+ * 本地检查 1b: 重依赖供给 (设计 §3.2 checking)。`@lancedb/lancedb` 与
151
+ * `@huggingface/transformers` 逐个判定 — entries 提供宿主入口路径 = 可用
152
+ * (不触自解析); 未提供时 resolveModule 自解析成功 = 可用, 抛错 = 计入缺失。
153
+ * 返回缺失 specifier 列表 (空 = 通过)。纯函数零 IO; 导出仅供测试 (与宿主侧
154
+ * buildHostDependencyAliases 同一模式), 生产接线传插件位置自解析 — 把加载步
155
+ * 的 "Cannot find module" 泛化包装前置成精准缺供文案, 省掉模型下载等前置工作。
156
+ */
157
+ export function missingHeavyDependencyModules(entries, resolveModule) {
158
+ const missing = [];
159
+ const supplies = [
160
+ ["@lancedb/lancedb", entries.lancedb],
161
+ ["@huggingface/transformers", entries.transformers],
162
+ ];
163
+ for (const [specifier, entryPath] of supplies) {
164
+ if (entryPath !== undefined)
165
+ continue;
166
+ try {
167
+ resolveModule(specifier);
168
+ }
169
+ catch {
170
+ missing.push(specifier);
171
+ }
172
+ }
173
+ return missing;
174
+ }
175
+ /**
176
+ * transformers.js 缓存布局的容错存在性检查 (设计 §3.2 手动导入路径):
177
+ * `<cacheDir>/<modelId>/` 下需有非空 tokenizer 文件与 `onnx/` 权重文件。
178
+ * 返回 undefined = 检查通过; "missing" = 目录/文件缺失或仅剩 0 字节残骸
179
+ * (失败下载产物, 清理后进入下载阶段, 见 model-cache-hygiene); 其他字符串 =
180
+ * 文件存在但可疑 (截断/非空过小), 属失败。
181
+ */
182
+ export function modelFilesCheck(cacheDir, modelId, minWeightBytes) {
183
+ const modelDir = join(cacheDir, modelId);
184
+ if (!existsSync(modelDir))
185
+ return "missing";
186
+ // tokenizer 只剩 0 字节文件 = 失败下载残骸 (真 tokenizer.json 不可能为空):
187
+ // 清掉按缺失处理可重下; 非空文件不动 (手动导入布局不受影响)。
188
+ const tokenizerNames = ["tokenizer.json", "tokenizer.model"].filter((name) => existsSync(join(modelDir, name)));
189
+ const hasTokenizer = tokenizerNames.some((name) => statSync(join(modelDir, name)).size > 0);
190
+ if (!hasTokenizer) {
191
+ if (tokenizerNames.length > 0)
192
+ purgeZeroByteCacheArtifacts(modelDir);
193
+ return "missing";
194
+ }
195
+ const onnxDir = join(modelDir, "onnx");
196
+ if (!existsSync(onnxDir))
197
+ return `onnx weight directory missing under ${modelDir}`;
198
+ const weights = readdirSync(onnxDir).filter((name) => name.includes(".onnx"));
199
+ if (weights.length === 0)
200
+ return `no onnx weight file under ${onnxDir}`;
201
+ const largest = Math.max(...weights.map((name) => statSync(join(onnxDir, name)).size));
202
+ if (largest < minWeightBytes) {
203
+ return `largest onnx weight under ${onnxDir} is ${largest} bytes, below the expected minimum ${minWeightBytes} — the download looks truncated`;
204
+ }
205
+ return undefined;
206
+ }
207
+ /**
208
+ * 本地检查 3: 磁盘余量 (设计 §3.2 checking, node:fs statfs, 零新依赖)。
209
+ * 以 agentDir 所在卷为准 (cacheDir 同卷创建)。agentDir 可能尚不存在 (全新
210
+ * 机器首启, 下载前无人创建过它 — 2026-10-06 live E2E 实测踩中) — statfs
211
+ * ENOENT 时逐级上溯最近存在的祖先目录, 同卷即同余量; 上溯到根仍失败才按
212
+ * 无法判定报告。
213
+ */
214
+ export async function diskHeadroomCheck(agentDir, minimumBytes) {
215
+ let freeBytes;
216
+ let probe = agentDir;
217
+ for (let attempt = 0; probe !== undefined && attempt < 16; attempt++) {
218
+ try {
219
+ const stats = await statfs(probe);
220
+ freeBytes = stats.bfree * stats.bsize;
221
+ break;
222
+ }
223
+ catch (error) {
224
+ if (!(error instanceof Error) || !String(error.message).includes("ENOENT")) {
225
+ return `disk free space could not be determined for ${agentDir} (probing ${probe}: ${error instanceof Error ? error.message : String(error)})`;
226
+ }
227
+ const parent = dirname(probe);
228
+ probe = parent === probe ? undefined : parent;
229
+ }
230
+ }
231
+ if (freeBytes === undefined) {
232
+ return `disk free space could not be determined for ${agentDir} (no existing ancestor directory to statfs)`;
233
+ }
234
+ if (freeBytes < minimumBytes) {
235
+ return `disk free space is ${freeBytes} bytes, below the required ${minimumBytes}`;
236
+ }
237
+ return undefined;
238
+ }
239
+ /** enabling 失败的五选一行动清单 (设计 §3.2 硬性要求)。 */
240
+ export function enablingActionList(modelCacheDir, currentMode) {
241
+ return [
242
+ `Switch to a reachable mirror: set ${HF_MIRRORS_ENV} to a comma-separated host list (tried before the defaults)`,
243
+ `Install the heavy dependencies in the host project (npm i onnxruntime-node @lancedb/lancedb @huggingface/transformers) so the host can supply them via hostDependencyPaths, then restart the session`,
244
+ `Download the models manually and place them under ${modelCacheDir} (layout: <modelId>/tokenizer.json + <modelId>/onnx/model*.onnx)`,
245
+ currentMode === "full"
246
+ ? 'Downgrade to memory.mode "light" (much smaller models)'
247
+ : 'Keep memory.mode "light" and retry with a reachable mirror',
248
+ 'Give up: leave memory.mode "off" (the default) so the memory family stays unregistered',
249
+ ];
250
+ }
251
+ // ---------------------------------------------------------------------------
252
+ // 征询持久化 (capability 拆分战役 slice 3b 随迁, 征询域): agent-dir 配置文件
253
+ // 的读写与征询拒绝/档位落盘。纯函数, 路径域显式传入; capability.ts 经
254
+ // re-export 维持历史导出面, 向量通道 (vector-channel.ts) 直接 import。
255
+ // ---------------------------------------------------------------------------
256
+ /** agent-dir 配置文件里的征询拒绝标记值 (memoryConsent 键)。 */
257
+ export const MEMORY_CONSENT_DECLINED_VALUE = "declined";
258
+ /** memory 插件 agent-dir 配置通道 (entry 读、capability 写, 同一路径)。 */
259
+ export const MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH = join("capabilities", "builtin.memory.json");
260
+ function readMemoryAgentDirConfig(agentDir) {
261
+ const configPath = join(agentDir, MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH);
262
+ if (existsSync(configPath)) {
263
+ try {
264
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
265
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
266
+ return parsed;
267
+ }
268
+ }
269
+ catch {
270
+ // 既有文件非法时以本次写入为准 (持久化是显式用户动作, 不被脏文件阻塞)。
271
+ }
272
+ }
273
+ return {};
274
+ }
275
+ /**
276
+ * 写 agent-dir 配置的原子替换 (2026-10-06 复审 P1-2): temp 文件 + rename —
277
+ * 自动升档引入了无人值守写者, 与其它进程的 /memory off (会删档位键) 读改写
278
+ * 交错时, 直接 writeFileSync 可能留下半截 JSON 让并发读者拿到空配置 (整块
279
+ * 丢键: 抹掉并发 decline 或升档)。rename 在同一目录内原子替换, 读者要么见
280
+ * 旧文件要么见新文件。
281
+ */
282
+ function writeMemoryAgentDirConfig(agentDir, config) {
283
+ const configPath = join(agentDir, MEMORY_AGENT_DIR_CONFIG_RELATIVE_PATH);
284
+ mkdirSync(dirname(configPath), { recursive: true });
285
+ const tempPath = `${configPath}.${process.pid}.tmp`;
286
+ writeFileSync(tempPath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
287
+ renameSync(tempPath, configPath);
288
+ }
289
+ /** agent-dir 配置里用户显式持久化的档位键 (无 / 该键即 undefined)。 */
290
+ export function readMemorySettingsMode(agentDir) {
291
+ const value = readMemoryAgentDirConfig(agentDir).memorySettingsMode;
292
+ return value === "light" || value === "full" ? value : undefined;
293
+ }
294
+ /**
295
+ * 拒绝/跳过的持久化 (2026-10-05 用户裁决): 写入插件自己的 agent-dir 配置文件,
296
+ * 不改写用户 settings.json。语义是「用户对默认档说了不」——仅抑制默认档;
297
+ * 用户日后显式设置 settings.json `memory.mode` (light/full) 时显式值仍然生效。
298
+ * 同时移除 /memory light|full 持久化的档位键, 保证"先升 full 再 off"不会留下
299
+ * 一个压过 settings 的陈旧档位。
300
+ */
301
+ export function writeMemoryConsentDeclined(agentDir) {
302
+ const config = readMemoryAgentDirConfig(agentDir);
303
+ config.memoryConsent = MEMORY_CONSENT_DECLINED_VALUE;
304
+ delete config.memorySettingsMode;
305
+ delete config.memoryMode;
306
+ writeMemoryAgentDirConfig(agentDir, config);
307
+ }
308
+ /**
309
+ * 档位升级/降级的持久化 (/memory light|full): 写入插件 agent-dir 配置的
310
+ * `memorySettingsMode` 键——与 settings.json `memory.mode` 同一解析层 (持久
311
+ * 配置面, env 仍可临时覆盖)。插件无 settings.json 写通道 (HostCapabilities
312
+ * 无该面), 本文件键在其上, 即命令是比 settings 更近一次的显式表达。同时清
313
+ * 除拒绝标记: 显式升/降档是最新的用户意图, 压过此前的"对默认档说不"。
314
+ */
315
+ export function writeMemorySettingsMode(agentDir, mode) {
316
+ const config = readMemoryAgentDirConfig(agentDir);
317
+ config.memorySettingsMode = mode;
318
+ delete config.memoryConsent;
319
+ writeMemoryAgentDirConfig(agentDir, config);
320
+ }
321
+ //# sourceMappingURL=enabling.js.map