@dsh-edge/dsh-edge-agent 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +372 -2
  4. package/config/edge.example.json +179 -0
  5. package/config/privacy-policy.json +202 -0
  6. package/cordis.desktop-safe.patch.yml +129 -0
  7. package/cordis.patch.yml +382 -0
  8. package/cordis.yml +374 -0
  9. package/lib/attestation.d.ts +2 -0
  10. package/lib/attestation.d.ts.map +1 -0
  11. package/lib/attestation.js +2 -0
  12. package/lib/audit-log.d.ts +2 -0
  13. package/lib/audit-log.d.ts.map +1 -0
  14. package/lib/audit-log.js +2 -0
  15. package/lib/client.js +87 -0
  16. package/lib/cloud-llm-adapter.d.ts +2 -0
  17. package/lib/cloud-llm-adapter.d.ts.map +1 -0
  18. package/lib/cloud-llm-adapter.js +2 -0
  19. package/lib/dsh-adapter/attestation.d.ts +42 -0
  20. package/lib/dsh-adapter/attestation.d.ts.map +1 -0
  21. package/lib/dsh-adapter/attestation.js +40 -0
  22. package/lib/dsh-adapter/audit-log.d.ts +139 -0
  23. package/lib/dsh-adapter/audit-log.d.ts.map +1 -0
  24. package/lib/dsh-adapter/audit-log.js +376 -0
  25. package/lib/dsh-adapter/cloud-egress.d.ts +181 -0
  26. package/lib/dsh-adapter/cloud-egress.d.ts.map +1 -0
  27. package/lib/dsh-adapter/cloud-egress.js +257 -0
  28. package/lib/dsh-adapter/cloud-llm-adapter.d.ts +416 -0
  29. package/lib/dsh-adapter/cloud-llm-adapter.d.ts.map +1 -0
  30. package/lib/dsh-adapter/cloud-llm-adapter.js +1472 -0
  31. package/lib/dsh-adapter/contract.d.ts +80 -0
  32. package/lib/dsh-adapter/contract.d.ts.map +1 -0
  33. package/lib/dsh-adapter/contract.js +194 -0
  34. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts +214 -0
  35. package/lib/dsh-adapter/desktop-cookie-janitor.d.ts.map +1 -0
  36. package/lib/dsh-adapter/desktop-cookie-janitor.js +409 -0
  37. package/lib/dsh-adapter/dsh-edge-agent.d.ts +48 -0
  38. package/lib/dsh-adapter/dsh-edge-agent.d.ts.map +1 -0
  39. package/lib/dsh-adapter/dsh-edge-agent.js +64 -0
  40. package/lib/dsh-adapter/edge-config.d.ts +484 -0
  41. package/lib/dsh-adapter/edge-config.d.ts.map +1 -0
  42. package/lib/dsh-adapter/edge-config.js +898 -0
  43. package/lib/dsh-adapter/edge-context-view.d.ts +184 -0
  44. package/lib/dsh-adapter/edge-context-view.d.ts.map +1 -0
  45. package/lib/dsh-adapter/edge-context-view.js +483 -0
  46. package/lib/dsh-adapter/effective-model.d.ts +85 -0
  47. package/lib/dsh-adapter/effective-model.d.ts.map +1 -0
  48. package/lib/dsh-adapter/effective-model.js +171 -0
  49. package/lib/dsh-adapter/etscli-shell-guard.d.ts +244 -0
  50. package/lib/dsh-adapter/etscli-shell-guard.d.ts.map +1 -0
  51. package/lib/dsh-adapter/etscli-shell-guard.js +420 -0
  52. package/lib/dsh-adapter/etscli-tools.d.ts +112 -0
  53. package/lib/dsh-adapter/etscli-tools.d.ts.map +1 -0
  54. package/lib/dsh-adapter/etscli-tools.js +393 -0
  55. package/lib/dsh-adapter/file-logger.d.ts +294 -0
  56. package/lib/dsh-adapter/file-logger.d.ts.map +1 -0
  57. package/lib/dsh-adapter/file-logger.js +564 -0
  58. package/lib/dsh-adapter/function-call-external.d.ts +357 -0
  59. package/lib/dsh-adapter/function-call-external.d.ts.map +1 -0
  60. package/lib/dsh-adapter/function-call-external.js +1005 -0
  61. package/lib/dsh-adapter/host-egress-guard.d.ts +161 -0
  62. package/lib/dsh-adapter/host-egress-guard.d.ts.map +1 -0
  63. package/lib/dsh-adapter/host-egress-guard.js +213 -0
  64. package/lib/dsh-adapter/host-llm-trace.d.ts +198 -0
  65. package/lib/dsh-adapter/host-llm-trace.d.ts.map +1 -0
  66. package/lib/dsh-adapter/host-llm-trace.js +344 -0
  67. package/lib/dsh-adapter/http-api-routes.d.ts +61 -0
  68. package/lib/dsh-adapter/http-api-routes.d.ts.map +1 -0
  69. package/lib/dsh-adapter/http-api-routes.js +79 -0
  70. package/lib/dsh-adapter/http-api-sessions.d.ts +105 -0
  71. package/lib/dsh-adapter/http-api-sessions.d.ts.map +1 -0
  72. package/lib/dsh-adapter/http-api-sessions.js +173 -0
  73. package/lib/dsh-adapter/http-api.d.ts +221 -0
  74. package/lib/dsh-adapter/http-api.d.ts.map +1 -0
  75. package/lib/dsh-adapter/http-api.js +1811 -0
  76. package/lib/dsh-adapter/index.d.ts +62 -0
  77. package/lib/dsh-adapter/index.d.ts.map +1 -0
  78. package/lib/dsh-adapter/index.js +97 -0
  79. package/lib/dsh-adapter/input-guard-messages.d.ts +56 -0
  80. package/lib/dsh-adapter/input-guard-messages.d.ts.map +1 -0
  81. package/lib/dsh-adapter/input-guard-messages.js +136 -0
  82. package/lib/dsh-adapter/input-guard.d.ts +508 -0
  83. package/lib/dsh-adapter/input-guard.d.ts.map +1 -0
  84. package/lib/dsh-adapter/input-guard.js +2174 -0
  85. package/lib/dsh-adapter/intent-prompts.d.ts +77 -0
  86. package/lib/dsh-adapter/intent-prompts.d.ts.map +1 -0
  87. package/lib/dsh-adapter/intent-prompts.js +97 -0
  88. package/lib/dsh-adapter/intent-router.d.ts +710 -0
  89. package/lib/dsh-adapter/intent-router.d.ts.map +1 -0
  90. package/lib/dsh-adapter/intent-router.js +2802 -0
  91. package/lib/dsh-adapter/intent-signals.d.ts +208 -0
  92. package/lib/dsh-adapter/intent-signals.d.ts.map +1 -0
  93. package/lib/dsh-adapter/intent-signals.js +391 -0
  94. package/lib/dsh-adapter/invariant-install.d.ts +16 -0
  95. package/lib/dsh-adapter/invariant-install.d.ts.map +1 -0
  96. package/lib/dsh-adapter/invariant-install.js +15 -0
  97. package/lib/dsh-adapter/invariant.d.ts +16 -0
  98. package/lib/dsh-adapter/invariant.d.ts.map +1 -0
  99. package/lib/dsh-adapter/invariant.js +22 -0
  100. package/lib/dsh-adapter/llm-adapters.d.ts +19 -0
  101. package/lib/dsh-adapter/llm-adapters.d.ts.map +1 -0
  102. package/lib/dsh-adapter/llm-adapters.js +16 -0
  103. package/lib/dsh-adapter/llm-internal-caller.d.ts +92 -0
  104. package/lib/dsh-adapter/llm-internal-caller.d.ts.map +1 -0
  105. package/lib/dsh-adapter/llm-internal-caller.js +100 -0
  106. package/lib/dsh-adapter/llm-max-turns.d.ts +55 -0
  107. package/lib/dsh-adapter/llm-max-turns.d.ts.map +1 -0
  108. package/lib/dsh-adapter/llm-max-turns.js +127 -0
  109. package/lib/dsh-adapter/llm-trace-store.d.ts +131 -0
  110. package/lib/dsh-adapter/llm-trace-store.d.ts.map +1 -0
  111. package/lib/dsh-adapter/llm-trace-store.js +205 -0
  112. package/lib/dsh-adapter/llm-trace.d.ts +450 -0
  113. package/lib/dsh-adapter/llm-trace.d.ts.map +1 -0
  114. package/lib/dsh-adapter/llm-trace.js +463 -0
  115. package/lib/dsh-adapter/local-llm-adapter.d.ts +141 -0
  116. package/lib/dsh-adapter/local-llm-adapter.d.ts.map +1 -0
  117. package/lib/dsh-adapter/local-llm-adapter.js +787 -0
  118. package/lib/dsh-adapter/local-storage.d.ts +96 -0
  119. package/lib/dsh-adapter/local-storage.d.ts.map +1 -0
  120. package/lib/dsh-adapter/local-storage.js +198 -0
  121. package/lib/dsh-adapter/model-router.d.ts +79 -0
  122. package/lib/dsh-adapter/model-router.d.ts.map +1 -0
  123. package/lib/dsh-adapter/model-router.js +107 -0
  124. package/lib/dsh-adapter/ohttp-gateway.d.ts +49 -0
  125. package/lib/dsh-adapter/ohttp-gateway.d.ts.map +1 -0
  126. package/lib/dsh-adapter/ohttp-gateway.js +32 -0
  127. package/lib/dsh-adapter/openai-sse.d.ts +85 -0
  128. package/lib/dsh-adapter/openai-sse.d.ts.map +1 -0
  129. package/lib/dsh-adapter/openai-sse.js +462 -0
  130. package/lib/dsh-adapter/pii-utils.d.ts +96 -0
  131. package/lib/dsh-adapter/pii-utils.d.ts.map +1 -0
  132. package/lib/dsh-adapter/pii-utils.js +324 -0
  133. package/lib/dsh-adapter/plugin-host.d.ts +38 -0
  134. package/lib/dsh-adapter/plugin-host.d.ts.map +1 -0
  135. package/lib/dsh-adapter/plugin-host.js +38 -0
  136. package/lib/dsh-adapter/plugin.d.ts +46 -0
  137. package/lib/dsh-adapter/plugin.d.ts.map +1 -0
  138. package/lib/dsh-adapter/plugin.js +100 -0
  139. package/lib/dsh-adapter/privacy-policy.d.ts +237 -0
  140. package/lib/dsh-adapter/privacy-policy.d.ts.map +1 -0
  141. package/lib/dsh-adapter/privacy-policy.js +391 -0
  142. package/lib/dsh-adapter/prompt-guard.d.ts +152 -0
  143. package/lib/dsh-adapter/prompt-guard.d.ts.map +1 -0
  144. package/lib/dsh-adapter/prompt-guard.js +872 -0
  145. package/lib/dsh-adapter/realm-skill-bridge.d.ts +365 -0
  146. package/lib/dsh-adapter/realm-skill-bridge.d.ts.map +1 -0
  147. package/lib/dsh-adapter/realm-skill-bridge.js +1180 -0
  148. package/lib/dsh-adapter/sanitizer-pipeline.d.ts +140 -0
  149. package/lib/dsh-adapter/sanitizer-pipeline.d.ts.map +1 -0
  150. package/lib/dsh-adapter/sanitizer-pipeline.js +431 -0
  151. package/lib/dsh-adapter/secure-bridge.d.ts +47 -0
  152. package/lib/dsh-adapter/secure-bridge.d.ts.map +1 -0
  153. package/lib/dsh-adapter/secure-bridge.js +35 -0
  154. package/lib/dsh-adapter/session-attribution.d.ts +70 -0
  155. package/lib/dsh-adapter/session-attribution.d.ts.map +1 -0
  156. package/lib/dsh-adapter/session-attribution.js +76 -0
  157. package/lib/dsh-adapter/sessions.d.ts +86 -0
  158. package/lib/dsh-adapter/sessions.d.ts.map +1 -0
  159. package/lib/dsh-adapter/sessions.js +179 -0
  160. package/lib/dsh-adapter/tcm.d.ts +55 -0
  161. package/lib/dsh-adapter/tcm.d.ts.map +1 -0
  162. package/lib/dsh-adapter/tcm.js +94 -0
  163. package/lib/dsh-adapter/td-bus.d.ts +87 -0
  164. package/lib/dsh-adapter/td-bus.d.ts.map +1 -0
  165. package/lib/dsh-adapter/td-bus.js +196 -0
  166. package/lib/dsh-adapter/tool-runner.d.ts +185 -0
  167. package/lib/dsh-adapter/tool-runner.d.ts.map +1 -0
  168. package/lib/dsh-adapter/tool-runner.js +271 -0
  169. package/lib/dsh-adapter/tools.d.ts +118 -0
  170. package/lib/dsh-adapter/tools.d.ts.map +1 -0
  171. package/lib/dsh-adapter/tools.js +292 -0
  172. package/lib/dsh-adapter/types.d.ts +23 -0
  173. package/lib/dsh-adapter/types.d.ts.map +1 -0
  174. package/lib/dsh-adapter/types.js +16 -0
  175. package/lib/dsh-adapter/voice-input.d.ts +30 -0
  176. package/lib/dsh-adapter/voice-input.d.ts.map +1 -0
  177. package/lib/dsh-adapter/voice-input.js +34 -0
  178. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts +152 -0
  179. package/lib/dsh-adapter/web-search-tavily-bocha.d.ts.map +1 -0
  180. package/lib/dsh-adapter/web-search-tavily-bocha.js +289 -0
  181. package/lib/http-api.d.ts +2 -0
  182. package/lib/http-api.d.ts.map +1 -0
  183. package/lib/http-api.js +2 -0
  184. package/lib/index.d.ts +48 -0
  185. package/lib/index.d.ts.map +1 -0
  186. package/lib/index.js +49 -0
  187. package/lib/intent-router.d.ts +2 -0
  188. package/lib/intent-router.d.ts.map +1 -0
  189. package/lib/intent-router.js +2 -0
  190. package/lib/llm-adapters.d.ts +2 -0
  191. package/lib/llm-adapters.d.ts.map +1 -0
  192. package/lib/llm-adapters.js +2 -0
  193. package/lib/local-llm-adapter.d.ts +2 -0
  194. package/lib/local-llm-adapter.d.ts.map +1 -0
  195. package/lib/local-llm-adapter.js +2 -0
  196. package/lib/local-storage.d.ts +2 -0
  197. package/lib/local-storage.d.ts.map +1 -0
  198. package/lib/local-storage.js +2 -0
  199. package/lib/model-router.d.ts +2 -0
  200. package/lib/model-router.d.ts.map +1 -0
  201. package/lib/model-router.js +2 -0
  202. package/lib/ohttp-gateway.d.ts +2 -0
  203. package/lib/ohttp-gateway.d.ts.map +1 -0
  204. package/lib/ohttp-gateway.js +2 -0
  205. package/lib/plugin-host.d.ts +2 -0
  206. package/lib/plugin-host.d.ts.map +1 -0
  207. package/lib/plugin-host.js +2 -0
  208. package/lib/privacy-policy.d.ts +2 -0
  209. package/lib/privacy-policy.d.ts.map +1 -0
  210. package/lib/privacy-policy.js +2 -0
  211. package/lib/prompt-guard.d.ts +2 -0
  212. package/lib/prompt-guard.d.ts.map +1 -0
  213. package/lib/prompt-guard.js +2 -0
  214. package/lib/sanitizer-pipeline.d.ts +2 -0
  215. package/lib/sanitizer-pipeline.d.ts.map +1 -0
  216. package/lib/sanitizer-pipeline.js +2 -0
  217. package/lib/secure-bridge.d.ts +2 -0
  218. package/lib/secure-bridge.d.ts.map +1 -0
  219. package/lib/secure-bridge.js +2 -0
  220. package/lib/sessions.d.ts +2 -0
  221. package/lib/sessions.d.ts.map +1 -0
  222. package/lib/sessions.js +2 -0
  223. package/lib/tcm.d.ts +2 -0
  224. package/lib/tcm.d.ts.map +1 -0
  225. package/lib/tcm.js +2 -0
  226. package/lib/td-bus.d.ts +2 -0
  227. package/lib/td-bus.d.ts.map +1 -0
  228. package/lib/td-bus.js +2 -0
  229. package/lib/tools.d.ts +2 -0
  230. package/lib/tools.d.ts.map +1 -0
  231. package/lib/tools.js +2 -0
  232. package/lib/voice-input.d.ts +2 -0
  233. package/lib/voice-input.d.ts.map +1 -0
  234. package/lib/voice-input.js +2 -0
  235. package/package.json +341 -3
  236. package/presets/agent-preset-edge.json +50 -0
  237. package/presets/edge.desktop-safe.patch.yml +503 -0
  238. package/presets/edge.patch.yml +557 -0
@@ -0,0 +1,710 @@
1
+ /**
2
+ * intent-router — LLM-based intent classification.
3
+ *
4
+ * Uses the local Qwen3-4B (via ctx.llm) to classify user messages as
5
+ * LOCAL (file ops, device control, calculations) or CLOUD (knowledge,
6
+ * chat, creative tasks). Falls back to keyword rules when the LLM
7
+ * call fails (timeout, network error, unclear response).
8
+ *
9
+ * @module dsh-edge-agent/intent-router
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import { Service } from '@deepseek-ai/cordis';
13
+ import Schema from '@deepseek-ai/schemastery';
14
+ export declare const name = "intent-router";
15
+ export declare const inject: readonly ["llm", "auditLog", "privacyPolicy"];
16
+ /** Minimal interface our router needs from the audit-log service. */
17
+ interface AuditLike {
18
+ log(entry: {
19
+ level: 'debug' | 'info' | 'warn' | 'error' | 'security';
20
+ kind: string;
21
+ source: string;
22
+ payload: Record<string, unknown>;
23
+ }): Promise<unknown>;
24
+ }
25
+ /**
26
+ * §7.122: 端点是否落在本机。
27
+ *
28
+ * provider 名(`local-llm`)**不代表**落点 —— config/edge.json 的
29
+ * `llm.local.baseUrl` 可以是任何地址,包括公网。operator 要判断
30
+ * 「这轮原文有没有出网」,看的是落点,不是名字。
31
+ *
32
+ * §7.130: 主人裁决——「是否出网」的判据是 **category(哪个池)**,不是端点
33
+ * 落在哪台机。本函数仍然有用(§7.122 的分类器落点披露是备查信息),但它
34
+ * **不参与出网判断**:local 池是 operator 自有设施,端点不在本机也不算出网。
35
+ */
36
+ export declare function isLoopbackEndpoint(endpoint: string | undefined): boolean;
37
+ export interface Config {
38
+ /** Default realm when classification finds no rule. */
39
+ defaultCategory: 'LOCAL' | 'CLOUD' | 'SANITIZED';
40
+ /** Provider to use for intent classification. */
41
+ classificationProvider: string;
42
+ /** Model id passed alongside `classificationProvider`. */
43
+ classificationModel: string;
44
+ /** Minimum confidence the LLM must express to be trusted.
45
+ * Below this value, the router falls back to keyword rules
46
+ * (fail-open, not REJECT — see Phase B plan). */
47
+ confidenceThreshold: number;
48
+ /** §7.115 (2026-10-25): decision-model-only mode — false = the LLM
49
+ * classifier never runs; a jev skip goes straight to the rules tier +
50
+ * default LOCAL (fail-closed). Default true. */
51
+ llmFallback?: boolean;
52
+ /** Hard cap on a single classify() LLM call. The AbortController fires
53
+ * this many ms after the stream starts; on timeout the call throws
54
+ * and the caller falls through to the keyword/default path (fail-open).
55
+ * Default 4000ms; env DSH_EDGE_INTENT_LLM_TIMEOUT_MS overrides. */
56
+ llmTimeoutMs?: number;
57
+ /** Max number of recent messages to embed as multi-turn context.
58
+ * 0 disables context. Default 3. */
59
+ recentMessagesLimit?: number;
60
+ /** Hard cap on input text length. Past this, the router truncates the
61
+ * prompt input and audits `intent.classify.truncated`. Default 2000;
62
+ * env DSH_EDGE_INTENT_MAX_INPUT overrides. Defense-in-depth against
63
+ * pathologically long prompts overwhelming qwen3-4b. */
64
+ maxInputLength?: number;
65
+ /** Last-resort LOCAL-keyword list used only when privacy-policy is absent
66
+ * or has no matching rule. Default ships EN + CN. Override via env
67
+ * DSH_EDGE_INTENT_FALLBACK_KEYWORDS (comma-separated) to add JA / KO /
68
+ * ES / domain-specific terms without editing source. Replaces the
69
+ * built-in list entirely when set. */
70
+ fallbackKeywords?: string[];
71
+ /** §7.47 (2026-09-12): CLOUD-keyword list matched BEFORE fallbackKeywords.
72
+ * Catches queries that obviously need web/search/online info (新闻/news/
73
+ * 搜索/search/latest/最新/today/今天) and routes them to cloud LLM +
74
+ * web tools — without waiting for the LLM classifier (which can hang
75
+ * on Qwen3-4B). Default ships EN + CN built-in list. Override via env
76
+ * DSH_EDGE_INTENT_CLOUD_KEYWORDS (comma-sep) — appended to built-in.
77
+ * §7.108: the policy file's stages.intent.cloudKeywords ALSO appends
78
+ * (hot-reloadable) — preferred operator surface. */
79
+ cloudKeywords?: string[];
80
+ /** §7.58 (2026-09-16): LOCAL-entity list (密盒/ets/etscli built-in)
81
+ * matched before every keyword layer and used to override a non-LOCAL
82
+ * LLM verdict. Env DSH_EDGE_INTENT_LOCAL_ENTITIES (comma-sep) appends to
83
+ * built-in (NOT replaces), same semantics as cloudKeywords. §7.108:
84
+ * policy-file stages.intent.localEntities also appends (hot-reloadable). */
85
+ localEntities?: string[];
86
+ /** §7.124 (2026-10-27): 家庭设备操作正则层的扩展词表(追加到内置词表)。 */
87
+ deviceCommands?: {
88
+ devices?: string[];
89
+ actions?: string[];
90
+ };
91
+ /** §7.92.3 (2026-09-23) + §7.92.5 (2026-09-23): jev HTTP intent
92
+ * recognizer (port 8001). Toggle — default OFF (opt-in). When ON,
93
+ * jev runs BEFORE the LLM classifier and short-circuits high-
94
+ * confidence / low-risk verdicts. See
95
+ * ~/work/jev-branch/docs/api_reference.md for the upstream contract.
96
+ * §7.92.5 splits the full categories[] taxonomy into ≤16-intent
97
+ * batches (jev v0.3.0 hard cap) and fires them concurrently via
98
+ * Promise.all. Env DSH_EDGE_INTENT_JEV_ENABLED (literal 'true' activates). */
99
+ jevEnabled?: boolean;
100
+ /** §7.92.3: jev base URL. Env DSH_EDGE_INTENT_JEV_URL. */
101
+ jevUrl?: string;
102
+ /** §7.112 (2026-10-25): jev backend kind. 'local' = the ONNX /predict
103
+ * contract (default); 'bailian-decision' = Alibaba's decision-model
104
+ * /systemone structured-decision interface — one call carries the whole
105
+ * taxonomy as choice/score questions (no 16-intent cap, no chitchat
106
+ * markers). Env DSH_EDGE_INTENT_JEV_PROVIDER. */
107
+ jevProvider?: 'local' | 'bailian-decision';
108
+ /** §7.112: decision-model id (bailian-decision only, e.g.
109
+ * 'decision-model-preview'). Env DSH_EDGE_INTENT_JEV_MODEL. */
110
+ jevModel?: string;
111
+ /** §7.112: literal Authorization key (edge.json jev.apiKeyRef →
112
+ * secrets.*). Bailian MaaS requires it; local ONNX never did. */
113
+ jevApiKey?: string;
114
+ /** §7.112: 'raw' = `Authorization: <key>` (org systemone examples);
115
+ * 'bearer' = `Authorization: Bearer <key>` (compatible-mode
116
+ * convention). Default 'raw'. */
117
+ jevAuthScheme?: 'raw' | 'bearer';
118
+ /** §7.114 (2026-10-25): classify path appended to jevUrl. '/systemone'
119
+ * (default) = the MaaS deployments; a local MLX serve of the same
120
+ * contract exposes '/v1/classify' — point jevUrl at its /v1 root and
121
+ * set '/classify'. The decision model is a config-file-bound EXTERNAL
122
+ * service: url/path/model/key all live in edge.json, zero hardcoding. */
123
+ jevPath?: string;
124
+ /** §7.115: directional confidence gate — a LOCAL-leaning decision-model
125
+ * verdict adopts at this floor (default 0.5) instead of paying the LLM
126
+ * round-trip for a second opinion; non-LOCAL (data may leave the device)
127
+ * keeps the strict jevConfidenceThreshold. */
128
+ jevLocalAdoptFloor?: number;
129
+ /** §7.92.3: per-batch jev HTTP timeout in ms (applies to EACH batch
130
+ * fired by the §7.92.5 orchestrator). Env DSH_EDGE_INTENT_JEV_TIMEOUT_MS. */
131
+ jevTimeoutMs?: number;
132
+ /** §7.92.3: minimum jev confidence to adopt. Below → fall through to
133
+ * LLM. Env DSH_EDGE_INTENT_JEV_CONFIDENCE_THRESHOLD. */
134
+ jevConfidenceThreshold?: number;
135
+ /** §7.92.3: jev risk_score gate (≥ this value → REJECT). Env
136
+ * DSH_EDGE_INTENT_JEV_RISK_THRESHOLD. */
137
+ jevRiskThreshold?: number;
138
+ /** §7.92.6 (2026-09-24): circuit-breaker TTL (ms). When all 3
139
+ * jev batches fail (transport / parse / timeout) within one
140
+ * classify() call, the breaker opens and subsequent classify()
141
+ * calls skip jev entirely until TTL elapses. Any successful batch
142
+ * (HTTP 2xx + parse OK, even if action='fallback_llm') closes the
143
+ * breaker immediately. Env DSH_EDGE_INTENT_JEV_BREAKER_TTL_MS. */
144
+ jevBreakerTtlMs?: number;
145
+ /** §7.92.7 (2026-09-24): boot-time `/health` probe (toggle). When
146
+ * true, the service fires a single GET `${jevUrl}/health` (timeout
147
+ * = `jevBootProbeTimeoutMs`, default 2000ms) at `apply()` time. On
148
+ * non-2xx / network error / timeout the breaker is **pre-opened**
149
+ * with full TTL — first classify() short-circuits without paying
150
+ * the ~3 × jevTimeoutMs wall-clock. Default ON; operators set
151
+ * false to skip the probe (e.g. when jev boots lazily alongside
152
+ * dsh-edge and is reliably up by the time chat starts). Env
153
+ * DSH_EDGE_INTENT_JEV_BOOT_PROBE_ENABLED. */
154
+ jevBootProbeEnabled?: boolean;
155
+ /** §7.92.7: boot probe timeout (ms). Smaller than `jevTimeoutMs`
156
+ * (which is per-batch for classify-time use). Env
157
+ * DSH_EDGE_INTENT_JEV_BOOT_PROBE_TIMEOUT_MS. */
158
+ jevBootProbeTimeoutMs?: number;
159
+ /** §7.92.7: interval (ms) between breaker-open `/health` probes.
160
+ * While the breaker is open, a setInterval pings `/health` every
161
+ * N ms (timeout = `jevProbeTimeoutMs`). On 2xx the breaker closes
162
+ * immediately (recovery can be faster than waiting for full TTL).
163
+ * Default 5000ms — frequent enough that recovery feels snappy,
164
+ * infrequent enough to avoid hammering a sick jev. Env
165
+ * DSH_EDGE_INTENT_JEV_BREAKER_PROBE_MS. */
166
+ jevBreakerProbeMs?: number;
167
+ /** §7.92.7: periodic probe timeout (ms). Smaller than `jevTimeoutMs`
168
+ * — we want fast fail on a sick jev rather than waiting the full
169
+ * classify-time budget. Env DSH_EDGE_INTENT_JEV_PROBE_TIMEOUT_MS. */
170
+ jevProbeTimeoutMs?: number;
171
+ /** §7.92.3 (2026-09-23): intent category specs (LOCAL/CLOUD/SANITIZED/
172
+ * REJECT descriptions + jev intent lists). Loaded from
173
+ * `ctx.privacyPolicy.getCategories()` at construction. When set, used
174
+ * as-is (test override path); when absent AND privacyPolicy is missing,
175
+ * falls back to a hardcoded built-in. Same shape as
176
+ * privacy-policy.json#categories: array of {id, description,
177
+ * examples?, jevIntents[]}. Operators extend by editing
178
+ * config/privacy-policy.json (hot-reload picks it up via
179
+ * `policy/loaded` event); no env var override. */
180
+ categories?: import('./privacy-policy.js').IntentCategory[];
181
+ }
182
+ export declare const Config: Schema<Config>;
183
+ export interface Intent {
184
+ category: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
185
+ confidence: number;
186
+ targetRealm: string;
187
+ rationale: string;
188
+ /**
189
+ * §7.123 (2026-10-27): provenance of `confidence`.
190
+ *
191
+ * `false` means "this number is a PLACEHOLDER, not a reported confidence"
192
+ * — today only the bailian decision-model path produces it: the backend
193
+ * returns a `choice` without a numeric confidence, so the decoder
194
+ * substitutes a synthetic 0.9 ("decisive-by-construction"). A synthetic
195
+ * value must not be able to satisfy an operator's *semantic trust*
196
+ * threshold (stages.intent.semanticCloudMinConfidence), because the
197
+ * operator is trusting a measurement that does not exist.
198
+ *
199
+ * `undefined` = reported (the normal case: LLM classifier, local ONNX
200
+ * jev, rules tier). Only `false` disables the semantic unlock.
201
+ */
202
+ confidenceReported?: boolean;
203
+ }
204
+ export interface IntentRouterService {
205
+ classify(text: string, context?: {
206
+ recentMessages?: unknown[];
207
+ sessionId?: string;
208
+ }): Promise<Intent>;
209
+ /**
210
+ * §7.107: does this text ask for PUBLIC-WEB information?
211
+ *
212
+ * The operator's ruling: the cloud LLM is only for "需要搜索公网上的信息".
213
+ * This is the signal that unlocks the cloud — a CLOUD verdict from any
214
+ * other tier (a confident local classifier, a caller route) is honoured
215
+ * only when this returns a keyword.
216
+ *
217
+ * The keyword list is the same one the rules tier uses (built-in
218
+ * information-acquisition words + `stages.intent.cloudKeywords`), so the
219
+ * gate and the rules tier can never disagree about what counts as a
220
+ * web need.
221
+ */
222
+ cloudSignal(text: string): string | undefined;
223
+ resolve(intent: Intent): {
224
+ realm: string;
225
+ config: Record<string, unknown>;
226
+ };
227
+ /** Snapshot of in-memory counters. O(1) read for ops dashboards. */
228
+ getStats(): IntentRouterStats;
229
+ }
230
+ /** Built-in last-resort LOCAL keyword list (EN + CN). Used only when
231
+ * privacy-policy is absent / empty AND `config.fallbackKeywords` is unset.
232
+ * Users override via env `DSH_EDGE_INTENT_FALLBACK_KEYWORDS` (comma-sep).
233
+ *
234
+ * §7.123 (2026-10-27): moved to `./intent-signals.ts` — the strategy DATA
235
+ * (keyword tables, entity tables, the fallback taxonomy) now lives apart
236
+ * from the decision LOGIC in this file. */
237
+ import { LOCAL_KEYWORDS_DEFAULT, CLOUD_KEYWORDS_DEFAULT, LOCAL_ENTITY_KEYWORDS_DEFAULT, JEV_MAX_INTENTS, CATEGORIES_FALLBACK, RULE_CATEGORIES } from './intent-signals.js';
238
+ export { LOCAL_KEYWORDS_DEFAULT, CLOUD_KEYWORDS_DEFAULT, LOCAL_ENTITY_KEYWORDS_DEFAULT, JEV_MAX_INTENTS, CATEGORIES_FALLBACK, RULE_CATEGORIES, };
239
+ export declare class IntentRouterServiceImpl extends Service {
240
+ private readonly defaultCategory;
241
+ private readonly classificationProvider;
242
+ private readonly classificationModel;
243
+ private readonly confidenceThreshold;
244
+ private readonly llmFallback;
245
+ private readonly llmTimeoutMs;
246
+ private readonly recentMessagesLimit;
247
+ private readonly maxInputLength;
248
+ private readonly fallbackKeywords;
249
+ /** §7.47 (2026-09-12): CLOUD keyword list, merged with built-in defaults.
250
+ * Matched in classifyWithRules BEFORE fallbackKeywords. */
251
+ private readonly cloudKeywords;
252
+ /** §7.58 (2026-09-16): LOCAL-entity list, merged with built-in defaults.
253
+ * Checked FIRST in classifyWithRules + as LLM-verdict override guardrail. */
254
+ private readonly localEntities;
255
+ /** §7.124: operator-extended device-command terms (ADDITIVE on top of the
256
+ * built-in vocabulary in ./intent-signals.ts). */
257
+ private readonly deviceCommandOverrides;
258
+ /** §7.92.3 (2026-09-23): jev HTTP intent recognizer (toggle). When
259
+ * true, classify() invokes classifyWithJev() BEFORE classifyWithLlm(). */
260
+ private readonly jevEnabled;
261
+ /** §7.92.3: jev base URL, e.g. `http://127.0.0.1:8001`. */
262
+ private readonly jevUrl;
263
+ /** §7.112: Bailian decision-model backend surface (see Config docs). */
264
+ private readonly jevProvider;
265
+ private readonly jevModel;
266
+ private readonly jevApiKey;
267
+ private readonly jevAuthScheme;
268
+ private readonly jevPath;
269
+ private readonly jevLocalAdoptFloor;
270
+ /** §7.92.3: jev fetch timeout (ms). */
271
+ private readonly jevTimeoutMs;
272
+ /** §7.92.3: min jev confidence to adopt (else fall through to LLM). */
273
+ private readonly jevConfidenceThreshold;
274
+ /** §7.92.3: jev risk_score gate (>= this → REJECT). */
275
+ private readonly jevRiskThreshold;
276
+ /** §7.92.6 (2026-09-24): circuit-breaker TTL (ms). When all 3
277
+ * batches fail, set `jevBreakerOpenUntil = Date.now() + ttl` and
278
+ * classify() short-circuits jev (zero fetch) until that timestamp.
279
+ * Any successful batch closes the breaker (sets to 0). */
280
+ private readonly jevBreakerTtlMs;
281
+ /** §7.92.6: ms-epoch timestamp until which the breaker is open.
282
+ * 0 = closed. Mutable; reset on every Promise.all settle. */
283
+ private jevBreakerOpenUntil;
284
+ /** §7.92.7: boot probe toggle. When true, `start()` fires one
285
+ * `GET ${jevUrl}/health` and pre-opens the breaker on failure. */
286
+ private readonly jevBootProbeEnabled;
287
+ /** §7.92.7: boot probe timeout (ms). */
288
+ private readonly jevBootProbeTimeoutMs;
289
+ /** §7.92.7: interval between breaker-open `/health` probes (ms).
290
+ * Only active while the breaker is open. */
291
+ private readonly jevBreakerProbeMs;
292
+ /** §7.92.7: periodic probe timeout (ms). */
293
+ private readonly jevProbeTimeoutMs;
294
+ /** §7.92.7: setInterval handle for the periodic recovery probe.
295
+ * null = no timer running (breaker closed or probe disabled). */
296
+ private jevBreakerProbeTimer;
297
+ /** §7.92.7: in-flight boot probe promise. Tracked so a test or
298
+ * shutdown hook can `await stop()` without leaving a dangling
299
+ * fetch racing the constructor's first classify() call. */
300
+ private jevBootProbePromise;
301
+ /** §7.92.3 (2026-09-23) + §7.108 (2026-10-24): categories spec — loaded
302
+ * from privacyPolicy.getCategories() at construction AND refreshed on
303
+ * every `policy/loaded` event when the spec came from the policy
304
+ * service (config.categories keeps its explicit-override semantics and
305
+ * is never refreshed). Drives both the LLM prompt (description field
306
+ * per category) AND the jev-intent → category lookup (jevIntents[] per
307
+ * category, flattened into jevIntentMap). Tests inject via
308
+ * `config.categories` to bypass privacy-policy. */
309
+ private categories;
310
+ /** §7.92.3: flat jev-intent-string → category lookup, derived from
311
+ * `this.categories[].jevIntents`. Rebuilt together with categories on
312
+ * policy hot-reload (§7.108). */
313
+ private jevIntentMap;
314
+ private readonly llm;
315
+ private readonly auditLog;
316
+ /** ── classify() 结果缓存(LRU + TTL)───────────────────────────────
317
+ * 只缓存"贵的"路径的最终 verdict(LLM 采纳 / jev 采纳,均过
318
+ * gateCloudVerdict);规则兜底、policy REJECT、jev-reject 都很便宜
319
+ * 且依赖可热重载的状态,不缓存。没有 in-flight 合并 —— 并发同文本
320
+ * 各自走 LLM(保持并发计数语义),只有"完成后"的重复才命中。
321
+ * verdict 对同一文本是确定性的(gate 也是纯文本判定),TTL 只为
322
+ * 让关键词表/策略热重载后的新判定尽快生效。 */
323
+ private static readonly CLASSIFY_CACHE_TTL_MS;
324
+ private static readonly CLASSIFY_CACHE_MAX;
325
+ private readonly classifyCache;
326
+ private classifyCacheKey;
327
+ private classifyCacheGet;
328
+ private classifyCacheSet;
329
+ private readonly stats;
330
+ constructor(ctx: Context, config: Config, auditLog?: AuditLike);
331
+ /**
332
+ * Soft service lookup that works in both Cordis (runtime) and unit tests (bare mock).
333
+ *
334
+ * Cordis context: `ctx.get(name)` is available — use it. Throws when the
335
+ * service is not in the `inject` list, so `get` acts as the gate.
336
+ * Bare mock context (tests): `ctx.get` is undefined — fall back to direct
337
+ * property access `ctx[name]`, which is how tests wire fake services.
338
+ *
339
+ * Either way, a missing service returns `undefined` rather than throwing.
340
+ */
341
+ private softService;
342
+ private audit;
343
+ /**
344
+ * §7.133: jev 决策模型调用的 trace。
345
+ *
346
+ * jev 走的是**裸 fetch**,不经过 `ctx.llm` —— 所以 adapter 那条路看不到
347
+ * 它。而它恰恰是把用户原文送出设备的那一call(当前配置指向百炼云端
348
+ * decision-model,§7.112/§7.114)。面板若只显示 local-llm 的分类器调用,
349
+ * 就是在显示一件没发生过的事(§7.107 的诚实性纪律)。
350
+ *
351
+ * 所以这里补一路 trace:caller 仍是 `intent-classifier`(它就是意图
352
+ * 分类器),provider 用 `bailian-decision` / `jev` 区分是哪条后端。
353
+ */
354
+ private traceJevRequest;
355
+ private traceJevResponse;
356
+ private emitJevTrace;
357
+ /**
358
+ * §7.122: 分类器 provider 的**真实落点**。
359
+ *
360
+ * provider 名(`local-llm`)不等于落点 —— config/edge.json 的
361
+ * `llm.local.baseUrl` 可以指向公网。审计要能回答「这轮原文发到哪去了」,
362
+ * 光有 provider 名不够。
363
+ *
364
+ * 解析规则:provider 以 `cloud-` 开头 → cloud 池;否则 local 池。
365
+ * (与 model-router 的 realm 判定同一口径:`local-*` 前缀 = LOCAL 域,
366
+ * 其余按 CLOUD 处理。)
367
+ */
368
+ private classifierEndpoint;
369
+ /** §7.113 (2026-10-25): the two evidence scalars the Edge tab renders
370
+ * beside the verdict — `keyword` = the regex keyword/entity the rules
371
+ * layer matched (derived from the rationale; empty for semantic
372
+ * verdicts), `semantic` = the chosen category's description sentence,
373
+ * i.e. the "why" the LLM/jev classifier answered with. Both are plain
374
+ * scalars so safePayload passes them (PII-scanned) to the intent panel. */
375
+ private classifyEvidence;
376
+ /** Redact PII from a short preview string before logging. Soft-dep on
377
+ * ctx.piiUtils.redactString. Returns `null` (rather than leaking) if
378
+ * redaction is unavailable — callers should log length-only in that case.
379
+ * Defense-in-depth: audit-log already runs redactObject on every payload,
380
+ * but router-level redaction survives audit-log misconfiguration. */
381
+ private redactPreview;
382
+ /**
383
+ * §7.107 (2026-10-23, operator ruling「只有需要搜索公网上的信息才需要用到
384
+ * cloudllm」): a CLOUD verdict needs a public-web need.
385
+ *
386
+ * The signal is this router's own cloud keyword list (built-in
387
+ * information-acquisition words + `stages.intent.cloudKeywords` + the
388
+ * policy file's `stages.intent.cloudKeywords`, §7.108) — the same list the
389
+ * rules tier matches, so the gate and the rules tier can never disagree
390
+ * about what counts as a web need.
391
+ *
392
+ * The gate is applied to the LLM / jev verdicts ONLY:
393
+ * - the rules tier reaches CLOUD either through a privacy-policy rule
394
+ * (the operator's own statement — honoured as-is) or through a cloud
395
+ * keyword (which IS the signal);
396
+ * - the LLM/jev verdicts are a MODEL's opinion, and a model's opinion
397
+ * is not what decides whether text may leave the device.
398
+ *
399
+ * It lives HERE — at the source of the verdict — and not in the
400
+ * consumer: the verdict is read by two consumers, input-guard
401
+ * (agent/pre-step) and http-api (which writes `agent._edgeRoute` from it
402
+ * and hands that over as an EXPLICIT route that input-guard honours
403
+ * as-is). A gate in the consumer closed only the input-guard path.
404
+ */
405
+ private gateCloudVerdict;
406
+ classify(text: string, context?: {
407
+ recentMessages?: unknown[];
408
+ sessionId?: string;
409
+ }): Promise<Intent>;
410
+ /** §7.92.5 (2026-09-23): build the full jev `intents[]` payload from
411
+ * `this.categories[]`. Two-phase priority, NO 16-cap (the cap is
412
+ * enforced one level up by `chunkIntents()`):
413
+ *
414
+ * Phase 1: emit every REJECT category's jevIntents first
415
+ * (security priority — must not be dropped)
416
+ * Phase 2: emit all other categories in `categories[]` config order
417
+ * (LOCAL → CLOUD → SANITIZED by default)
418
+ *
419
+ * Dedup by name via a Set<string> so operators who accidentally list
420
+ * the same intent in two categories don't produce a duplicate name
421
+ * (which would confuse jev's own zero-shot classifier).
422
+ *
423
+ * Returns the un-truncated array — the orchestrator batches it. */
424
+ private buildJevIntentsPayload;
425
+ /** §7.92.5 (2026-09-23): single-batch jev fetcher. Extracted from
426
+ * the old monolithic `classifyWithJev()` so the orchestrator can
427
+ * fire N concurrent calls via `Promise.all` and merge the results.
428
+ *
429
+ * Takes the same `text` + `sessionId` as the old function, plus an
430
+ * already-≤16 `intents[]` slice and `chitchatIntents` (defensive
431
+ * marker for jev v0.3.0+). Returns the same shape:
432
+ * - `action: 'reject' | 'adopt' | 'skip'`
433
+ * - `confidence` / `riskScore` / `latencyMs` (always set)
434
+ * - `skipReason` when action='skip' — caller can tell whether to
435
+ * retry the verdict on the next batch (we don't — we just merge)
436
+ *
437
+ * Every audit / counter side-effect for THIS batch fires here, not
438
+ * in the orchestrator, so partial failures are observable per-batch
439
+ * in the JSONL audit stream. */
440
+ private fetchJevBatch;
441
+ /** §7.92.5 (2026-09-23): jev HTTP prefilter — batched orchestrator.
442
+ * Splits the full `categories[].jevIntents[]` payload into ≤16-size
443
+ * batches (jev v0.3.0 hard cap from `head_max_len=256`), fires all
444
+ * batches concurrently via `Promise.all`, and merges the verdicts
445
+ * with a max-confidence pick:
446
+ *
447
+ * 1. Any `reject` action → pick highest `riskScore` (security signal)
448
+ * 2. Else any `adopt` action → pick highest `confidence`; tie-break
449
+ * by earlier batch index (REJECT-priority intents live in
450
+ * batch 0, so the security-first ordering is preserved on ties)
451
+ * 3. Else all skipped → return the first skip result
452
+ *
453
+ * Returns one of:
454
+ * - `action: 'reject'` → `risk_score >= jevRiskThreshold` (security-level)
455
+ * - `action: 'adopt'` → `data.action === 'execute'`, confidence ≥
456
+ * threshold, intent string found in jevIntentMap
457
+ * (mapped to realm)
458
+ * - `action: 'skip'` → any other condition (low-conf, fallback_llm,
459
+ * unrecognized intent, transport / parse error).
460
+ * Caller falls through to classifyWithLlm.
461
+ *
462
+ * Failure handling: every per-batch error path (timeout, network,
463
+ * parse) is audited at warn level and produces `action: 'skip'` for
464
+ * THAT batch — jev being unavailable MUST NOT degrade the existing
465
+ * LLM classification path. This is the same fail-open contract used
466
+ * by classifyWithLlm's classifyWithRules fallback.
467
+ *
468
+ * See ~/work/jev-branch/docs/api_reference.md for the wire spec.
469
+ * Implementation notes:
470
+ * - Uses global `fetch` (Node ≥18). AbortSignal for timeout.
471
+ * - No auth header — jev is local-network by design.
472
+ * - `metadata.session_id` for cross-service trace correlation.
473
+ * - `lang` hardcoded 'zh' (we don't auto-detect; the operator's UI
474
+ * can pass it later if multilingual queries become common). */
475
+ /** §7.112: headers for every jev HTTP surface (predict / health /
476
+ * systemone). Content-Type always; Authorization only when a key is
477
+ * configured — Bailian MaaS requires it, the local ONNX service never
478
+ * did. */
479
+ private jevHeaders;
480
+ /** §7.112 (2026-10-25): Bailian decision-model backend for the jev stage.
481
+ *
482
+ * One POST `${jevUrl}/systemone` replaces the batched /predict flow:
483
+ * the structured-decision interface carries the whole taxonomy at once —
484
+ * questions.intent = choice over categories (criteria = id → description)
485
+ * questions.risk = score over 4 danger levels (→ risk_score 0..1)
486
+ * so there is no 16-intent cap and no chitchat markers. Confidence/risk
487
+ * gates, breaker semantics and audit rows mirror the local flow so
488
+ * classify() cannot tell the backends apart. */
489
+ private classifyWithBailianDecision;
490
+ private classifyWithJev;
491
+ private classifyWithLlm;
492
+ /** §7.58 (2026-09-16): match a LOCAL-entity keyword. CJK entries are
493
+ * substring matches; ASCII entries require non-alphanumeric boundaries
494
+ * (so 'ets' never fires inside "assets"/"settings"). Returns the matched
495
+ * (lower-cased) keyword, or undefined. */
496
+ private matchLocalEntity;
497
+ /** §7.108 (2026-10-24): the policy file's stages.intent signal tables,
498
+ * read LIVE on every classify so a policy hot-reload takes effect on
499
+ * the next turn. Missing service / old policy → undefined (callers
500
+ * fall back to the constructor-built lists). */
501
+ private policyIntentStages;
502
+ /** §7.108: the FULL cloud signal list = built-in + patch config +
503
+ * policy stages.intent.cloudKeywords. ONE definition shared by the
504
+ * rules tier and the §7.107 gate — they can never disagree about what
505
+ * counts as a public-web need. */
506
+ private cloudKeywordsAll;
507
+ /** §7.108: the FULL LOCAL entity list = built-in + patch config + policy. */
508
+ private localEntitiesAll;
509
+ /** §7.108: LOCAL last-resort list — policy stages.intent.fallbackKeywords
510
+ * (non-empty) REPLACES, mirroring the env var's replace semantics. */
511
+ /** §7.124 (2026-10-27): the device-command extension terms = patch config
512
+ * (config/edge.json stages.intent.deviceCommands) + the policy file's
513
+ * stages.intent.deviceCommands (hot-reloadable, read LIVE per classify).
514
+ * Both are ADDITIVE on top of the built-in vocabulary. */
515
+ private deviceCommandTerms;
516
+ private fallbackKeywordsAll;
517
+ /**
518
+ * §7.107: the public-web signal — the keyword that unlocks the cloud.
519
+ *
520
+ * Same list the rules tier matches (built-in information-acquisition words
521
+ * + patch `stages.intent.cloudKeywords` + policy-file
522
+ * `stages.intent.cloudKeywords` (§7.108)); `classifyWithRules` calls this
523
+ * so the gate and the rules tier cannot drift apart.
524
+ */
525
+ cloudSignal(text: string): string | undefined;
526
+ private classifyWithRules;
527
+ resolve(intent: Intent): {
528
+ realm: string;
529
+ config: Record<string, unknown>;
530
+ };
531
+ /** Snapshot of in-memory counters. Returned object is a copy so callers
532
+ * can't mutate the live state. Counters are process-local — restart
533
+ * zeroes them. */
534
+ /** §7.92.7 (2026-09-24): boot-time health probe. Fires once at
535
+ * `start()` (called from `apply()`). Fire-and-forget — does NOT
536
+ * block plugin init. On 2xx → no-op (the breaker stays closed;
537
+ * classify() proceeds normally). On non-2xx / network error /
538
+ * timeout → pre-opens the breaker for `jevBreakerTtlMs` so the
539
+ * first classify() short-circuits instead of paying
540
+ * 3 × `jevTimeoutMs` wall-clock.
541
+ *
542
+ * Why fire-and-forget: blocking plugin init on a 2s jev probe
543
+ * adds 2s to cold boot, and a chat-less boot (e.g. CI smoke) has
544
+ * no classify() to gate. Race against first classify() is fine
545
+ * because: (1) plugin apply() runs before http-api opens its
546
+ * port, so the earliest chat is several hundred ms away, (2) if
547
+ * classify() runs first it just trips the breaker the normal
548
+ * way (3 batch fails) — same outcome as the probe. */
549
+ start(): void;
550
+ /** §7.92.7: dispose hook. Awaits the in-flight boot probe (if
551
+ * any) and clears the recovery-probe timer. Tests call this to
552
+ * keep the event loop clean between cases; cordis calls this
553
+ * on plugin teardown. */
554
+ stop(): Promise<void>;
555
+ /** §7.92.7: probe jev `/health`. Returns true on HTTP 2xx, false
556
+ * on anything else (network error, timeout, non-2xx). Always
557
+ * resolves — never throws. The `phase` arg distinguishes boot-time
558
+ * vs periodic-recovery audit kinds so ops can tell them apart in
559
+ * the log. */
560
+ private probeJevHealth;
561
+ /** §7.92.7: start the recovery probe setInterval. Idempotent.
562
+ * Called from classifyWithJev() right after `jevBreakerOpenUntil`
563
+ * is set. When the periodic probe lands on 2xx it closes the
564
+ * breaker (and stops itself) — recovery is faster than waiting
565
+ * for full TTL. */
566
+ private startBreakerProbe;
567
+ /** §7.92.7: clear the recovery probe setInterval. Idempotent. */
568
+ private stopBreakerProbe;
569
+ /** §7.92.7 helper: wall-clock spent in the most recent successful
570
+ * probe (used in `intent.jev.recovered` audit payload). The
571
+ * setInterval callback captures `probeStart` in its closure but
572
+ * doesn't surface it cleanly — keep this lightweight by storing
573
+ * the latest observed latency in instance state. */
574
+ private jevLastProbeLatencyMs;
575
+ private jevProbeTimerLatency;
576
+ getStats(): IntentRouterStats;
577
+ }
578
+ /** Map an Intent to a realm/config tuple the rest of the agent loop can consume. */
579
+ export declare function resolveIntent(intent: Intent): {
580
+ realm: string;
581
+ config: Record<string, unknown>;
582
+ };
583
+ /** Parsed shape of the LLM's classification response. */
584
+ interface ClassificationVerdict {
585
+ /** §7.92.3 (2026-09-23): widened from the historical 2-category union
586
+ * (LOCAL | CLOUD) to match the categories spec in privacy-policy.json
587
+ * — the LLM prompt now lists all 4 (LOCAL/CLOUD/SANITIZED/REJECT),
588
+ * so the verdict parser must accept them too. The downstream code
589
+ * (entity guardrail, classifyWithRules fallback, etc.) was already
590
+ * 4-category-aware via `RULE_CATEGORIES`. */
591
+ category: 'LOCAL' | 'CLOUD' | 'SANITIZED' | 'REJECT';
592
+ confidence: number;
593
+ }
594
+ /** In-memory counters exposed via getStats(). Pure observability; the audit
595
+ * log already carries every per-call kind, but counters give operators an
596
+ * O(1) read for dashboards / alerts (rate of timeouts, % sanitized, etc.)
597
+ * without parsing JSONL. */
598
+ export interface IntentRouterStats {
599
+ total: number;
600
+ empty: number;
601
+ truncated: number;
602
+ /** LLM verdict adopted as the final intent. */
603
+ llmHit: number;
604
+ /** TTL/LRU 缓存命中 —— 同文本+同上下文的重复 classify() 不再付
605
+ * LLM/jev 往返("你好" 类闲聊在 §7.107 gate 下尤其常见)。 */
606
+ cacheHit: number;
607
+ /** classifyWithRules adopted — privacy-policy match OR hardcoded keyword. */
608
+ ruleHit: number;
609
+ /** §7.124 (2026-10-27): Tier-0 device-command regex hit — the turn was
610
+ * decided locally by regex, without calling jev or the LLM classifier.
611
+ * Every hit saved one decision-model round-trip (~150ms-6s). */
612
+ deviceCommandHit: number;
613
+ /** No LLM verdict, no keyword → defaultCategory returned. */
614
+ defaultHit: number;
615
+ /** LLM returned below confidence threshold (cause of fallback to rules). */
616
+ lowConfidence: number;
617
+ /** AbortController fired during LLM stream. */
618
+ timeout: number;
619
+ /** LLM threw a non-timeout error (network, parse, etc.). */
620
+ llmError: number;
621
+ /** P1: LLM stream returned text that parseClassificationVerdict could
622
+ * not turn into a verdict (all three tiers failed). Distinct from
623
+ * `llmError` (network / timeout) — the LLM DID respond, the response
624
+ * just wasn't structured. Operators can compare
625
+ * `parseFailure / (llmHit + lowConfidence + parseFailure + llmError)`
626
+ * to gauge parse health. */
627
+ parseFailure: number;
628
+ /** REJECT verdict returned (pre-check or fallback). */
629
+ rejected: number;
630
+ /** Sanitizer applied to CLOUD-classification prompt. */
631
+ sanitized: number;
632
+ /** Sanitizer threw (fail-closed → throws, falls back to classifyWithRules). */
633
+ sanitizeError: number;
634
+ /** CLOUD realm but no sanitizer handle mounted (fail-closed → throws). */
635
+ sanitizeSkipped: number;
636
+ /** §7.92.3 (2026-09-23): jev verdict ADOPTED (path: jev-adopt). Sum
637
+ * with `llmHit + ruleHit + defaultHit + jevHit` to verify all paths
638
+ * hit a final verdict. Compare `jevHit / total` to estimate latency
639
+ * savings from the toggle (every jevHit replaces a 4-30s LLM call
640
+ * with a ~150ms jev call). */
641
+ jevHit: number;
642
+ /** §7.92.3: jev risk-gated REJECT (risk_score >= threshold). Subset
643
+ * of `rejected`. */
644
+ jevReject: number;
645
+ /** §7.92.3: jev SKIPPED to LLM (low-conf / fallback_llm / unmapped).
646
+ * Distinct from jevError (transport failure) — skip means jev
647
+ * worked fine, just deferred to LLM for quality reasons. */
648
+ jevSkip: number;
649
+ /** §7.92.3: jev transport / parse failure. Counts alongside jevSkip
650
+ * in classify()'s tally but operators triage them separately:
651
+ * jevError rate climbing → jev service health issue, jevSkip rate
652
+ * climbing → jev's intent set doesn't match our traffic. */
653
+ jevError: number;
654
+ /** §7.92.3: subset of jevError where AbortSignal fired. */
655
+ jevTimeout: number;
656
+ /** §7.92.3: cumulative jev `data.latency_ms` sum (ms). Use with
657
+ * `getStats()` calls to compute average jev latency without
658
+ * parsing JSONL. */
659
+ jevLatencyMsTotal: number;
660
+ /** §7.92.6 (2026-09-24): circuit-breaker opened N times in this
661
+ * process lifetime. Climbs → jev service is flapping. Pair with
662
+ * jevCircuitSkip to estimate total bypassed latency: each skip
663
+ * avoided ~3 × jevTimeoutMs = 7500ms worst-case wall-clock. */
664
+ jevCircuitOpen: number;
665
+ /** §7.92.6: classify() calls that returned early because breaker
666
+ * was open. Total latency saved ≈ jevCircuitSkip × 7500ms. */
667
+ jevCircuitSkip: number;
668
+ /** §7.92.7 (2026-09-24): boot probe counters. jevBootProbeOk
669
+ * climbs when the startup `GET /health` returned 2xx; jevBootProbeFail
670
+ * climbs when it failed (breaker pre-opened, first classify()
671
+ * short-circuits). Operators compare these to gauge whether jev
672
+ * is consistently unhealthy at boot. */
673
+ jevBootProbeOk: number;
674
+ jevBootProbeFail: number;
675
+ /** §7.92.7: periodic recovery probe counters. jevProbeAttempts =
676
+ * total `/health` pings fired while the breaker was open;
677
+ * jevRecoveredProbe = times a probe succeeded and closed the
678
+ * breaker (recovery was faster than waiting for full TTL).
679
+ * Healthy ratio ≈ jevRecoveredProbe / jevProbeAttempts. */
680
+ jevProbeAttempts: number;
681
+ jevRecoveredProbe: number;
682
+ /** G9: 10-bucket histogram of LLM confidence values × (accepted | rejected).
683
+ * Bucket key is `String(Math.floor(c * 10))` clamped to [0,9]. Use
684
+ * `getStats()` to obtain a deep copy. Empty until the first parsed
685
+ * verdict — timeouts / errors / unclear responses are not recorded. */
686
+ confidenceHistogram: {
687
+ [bucket: string]: {
688
+ accepted: number;
689
+ rejected: number;
690
+ };
691
+ };
692
+ }
693
+ /**
694
+ * Fence-tolerant parser for `{ category, confidence }` JSON the LLM returns.
695
+ * Falls back to: extract the first {...} block → regex token match for
696
+ * `category` + a best-effort confidence float from the surrounding text.
697
+ */
698
+ export declare function parseClassificationVerdict(raw: string): ClassificationVerdict | null;
699
+ export declare function coerceClassification(obj: unknown): ClassificationVerdict | null;
700
+ /** Best-effort extract a text payload from a recent-message object.
701
+ * Handles dsh-llm's `{content: [{type:'text', text}]}`, plain `{text}`,
702
+ * `{message: string}`, or anything stringifiable. Returns '' on failure. */
703
+ export declare function extractMessageText(m: unknown): string;
704
+ declare module '@deepseek-ai/cordis' {
705
+ interface Context {
706
+ intentRouter: IntentRouterServiceImpl;
707
+ }
708
+ }
709
+ export declare function apply(ctx: Context, config: Config): Promise<() => Promise<void>>;
710
+ //# sourceMappingURL=intent-router.d.ts.map