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

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 +158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -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 +253 -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 @@
1
+ {"version":3,"file":"openai-sse.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/openai-sse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,462 @@
1
+ /**
2
+ * openai-sse — shared helpers for talking to OpenAI-compatible
3
+ * `/chat/completions` endpoints. Both `cloud-llm-adapter` (DeepSeek)
4
+ * and `local-llm-adapter` (`mode: 'real'`) drive their wire envelope
5
+ * and SSE parse through this module so a wire-protocol change lands
6
+ * in one place.
7
+ *
8
+ * Wire shape:
9
+ * POST {baseUrl}/chat/completions
10
+ * { model, messages, stream: true, tools?, temperature?, max_tokens? }
11
+ * response: `text/event-stream` of `data: {...}` frames ending in
12
+ * `data: [DONE]`. The adapter translates frames into the DSH
13
+ * `StreamChunk` union (block-start, text-delta, reasoning-delta,
14
+ * tool-call-delta, block-end, usage, finish).
15
+ *
16
+ * @module dsh-edge-agent/openai-sse
17
+ */
18
+ import { attributionHeaders, LlmError, ToolCallId as brandCallId } from '@deepseek-ai/dsh-llm';
19
+ /**
20
+ * §7.105 (2026-10-21): an empty tool-call id is FATAL, not cosmetic.
21
+ *
22
+ * OpenAI-compatible endpoints may omit the tool-call `id` entirely (some
23
+ * llama.cpp / vLLM builds send `tool_calls` deltas with no `id` field) or
24
+ * send `id: ""`. The host persists that id verbatim into the v4 session log,
25
+ * and the log validator rejects the WHOLE FILE on the next load
26
+ * ("tool call id requires a nonempty string") — so ONE bad tool call makes
27
+ * the entire session history unloadable, not just the affected turn.
28
+ *
29
+ * Never let an empty id escape this module. The synthesized id is stored in
30
+ * the per-stream `toolCalls` map, which keeps it stable across every delta
31
+ * of one call and identical to the final `block-end` block, so the host's
32
+ * assistant message advertises exactly one id for the call.
33
+ */
34
+ function synthesizeToolCallId(wireIndex) {
35
+ return `call_local_${wireIndex}_${Math.random().toString(36).slice(2, 10)}`;
36
+ }
37
+ /**
38
+ * Pick the first candidate that is a NONEMPTY string, else `fallback`.
39
+ * `??` is not enough: an empty string survives `??` but is an invalid
40
+ * OpenAI `id` / `tool_call_id`.
41
+ */
42
+ function firstNonEmptyId(candidates, fallback) {
43
+ for (const candidate of candidates) {
44
+ if (typeof candidate === 'string' && candidate !== '')
45
+ return candidate;
46
+ }
47
+ return fallback;
48
+ }
49
+ /** Build the wire body for `POST /chat/completions`. */
50
+ export function buildChatCompletionsBody(options, defaultModel, temperature,
51
+ // §7.87.5 (2026-09-19): inject `chat_template_kwargs={enable_thinking: false}`
52
+ // for thinking models (qwen3.5 / QwQ / DeepSeek-R1). Default OFF — production
53
+ // wants fast non-reasoning answers; users can opt-in via env var.
54
+ enableThinking = false,
55
+ // §7.106 (2026-10-23): also send the standard `reasoning_effort:"none"` —
56
+ // the dialect OLLAMA's /v1 layer actually honors (probe-verified on the
57
+ // user's <内网主机>:11434: chat_template_kwargs AND top-level `think:false`
58
+ // are both silently dropped there; reasoning_effort:none removed
59
+ // message.reasoning and took completion 46→6 tokens). Without it the
60
+ // 256-token model-detect call has its budget eaten by reasoning and
61
+ // returns empty content (finish=length → json-parse-failed → fail-open).
62
+ // Only the local pool sets this — unknown-field-strict servers stay on
63
+ // the old dialect alone.
64
+ alsoReasoningEffort = false) {
65
+ const wireMessages = [];
66
+ if (options.system !== undefined && options.system.length > 0) {
67
+ wireMessages.push({ role: 'system', content: options.system });
68
+ }
69
+ // §7.105 (2026-10-21): resolve tool-call ids UP FRONT so an assistant
70
+ // tool-call and its tool-result always carry the SAME id on the wire.
71
+ //
72
+ // `??` lets an EMPTY id through (it is not nullish), and a tool-call id
73
+ // that is empty on the wire is rejected upstream. It is also the id the
74
+ // host persists into the session log — the v4 log validator rejects the
75
+ // WHOLE file on an empty tool-call id ("tool call id requires a nonempty
76
+ // string"), so one empty id makes an entire session fail to load.
77
+ //
78
+ // The blocks are deep-frozen at runtime, so the resolved id is carried in a
79
+ // side map instead of being written back. Results always follow their call
80
+ // in conversation order, so a FIFO queue pairs an id-less result with the
81
+ // id-less call it answers.
82
+ const resolvedIds = new Map();
83
+ const pendingEmptyCallIds = [];
84
+ for (const message of options.messages) {
85
+ const blocks = (Array.isArray(message.content) ? message.content : []);
86
+ for (const block of blocks) {
87
+ if (block.type === 'tool-call') {
88
+ const existing = firstNonEmptyId([block.toolCallId, block.callId, block.id], '');
89
+ if (existing !== '') {
90
+ resolvedIds.set(block, existing);
91
+ }
92
+ else {
93
+ const synthesized = `call_${Math.random().toString(36).slice(2)}`;
94
+ resolvedIds.set(block, synthesized);
95
+ pendingEmptyCallIds.push(synthesized);
96
+ }
97
+ }
98
+ else if (block.type === 'tool-result') {
99
+ const existing = firstNonEmptyId([block.toolCallId, block.callId, block.id], '');
100
+ if (existing !== '') {
101
+ resolvedIds.set(block, existing);
102
+ }
103
+ else {
104
+ resolvedIds.set(block, pendingEmptyCallIds.shift() ?? `call_${Math.random().toString(36).slice(2)}`);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ for (const message of options.messages) {
110
+ // Cordis messages use content-block arrays. We need to translate:
111
+ // - text blocks → concatenated text content
112
+ // - tool-call blocks → OpenAI `tool_calls` field (assistant messages)
113
+ // - tool-result blocks → separate `role: 'tool'` messages with `tool_call_id`
114
+ const blocks = (Array.isArray(message.content) ? message.content : []);
115
+ // §7.119 (2026-10-26): DSH 的**规范** tool-result 形状是「消息级」的 ——
116
+ // { role:'tool', toolCallId:'call_x', source:{kind:'tool',callId:'call_x'},
117
+ // content:[{type:'text', text:'…'}] }
118
+ // (上游 `createToolResultMessage()` 造的就是这个形状,见
119
+ // @deepseek-ai/dsh-llm lib/index.js。)
120
+ //
121
+ // 旧实现只认 content 里 `type==='tool-result'` 的块,于是把上面这条消息
122
+ // 当成普通消息处理:tool_call_id 直接丢失 → §7.106 的配对守卫(下面
123
+ // 205-215 行)判定它「找不到配对的 call」→ 降级成
124
+ // role:'user', content:"[tool-result (unreferenced) lost its matching
125
+ // call; output:]\n<真实输出>"
126
+ // 也就是说**模型的每一个工具结果都是以「丢失配对」的 user 文本进去的**,
127
+ // 而 assistant 侧的 tool_call 从来没被真正应答过。
128
+ //
129
+ // 实测:真实会话里 100% 的 tool/result 事件都是这个形状(77/77)——
130
+ // 整条工具结果通道是坏的,不是偶发。
131
+ //
132
+ // 这里补一条消息级分支:能取到 id 就按真正的 `role:'tool'` + `tool_call_id`
133
+ // 下发,取不到才回落到下面的块级路径(保持旧行为)。
134
+ if (message.role === 'tool') {
135
+ const messageId = firstNonEmptyId([
136
+ message.toolCallId,
137
+ message.source?.callId,
138
+ message.source?.toolCallId,
139
+ ], '');
140
+ if (messageId !== '') {
141
+ const toolText = typeof message.content === 'string'
142
+ ? message.content
143
+ : extractText(blocks);
144
+ wireMessages.push({ role: 'tool', tool_call_id: messageId, content: toolText });
145
+ continue;
146
+ }
147
+ }
148
+ const textParts = [];
149
+ const toolCalls = [];
150
+ const toolResults = [];
151
+ for (const block of blocks) {
152
+ if (block.type === 'text' && typeof block.text === 'string') {
153
+ textParts.push(block.text);
154
+ }
155
+ else if (block.type === 'tool-call') {
156
+ toolCalls.push({
157
+ id: resolvedIds.get(block) ?? firstNonEmptyId([block.toolCallId, block.callId, block.id], `call_${Math.random().toString(36).slice(2)}`),
158
+ type: 'function',
159
+ function: {
160
+ name: String(block.name ?? ''),
161
+ arguments: typeof block.arguments === 'string' ? block.arguments : JSON.stringify(block.arguments ?? {}),
162
+ },
163
+ });
164
+ }
165
+ else if (block.type === 'tool-result') {
166
+ toolResults.push({
167
+ id: resolvedIds.get(block) ?? firstNonEmptyId([block.toolCallId, block.callId, block.id], ''),
168
+ content: typeof block.content === 'string' ? block.content : extractText(block.content),
169
+ });
170
+ }
171
+ // reasoning blocks are intentionally skipped for the wire format
172
+ }
173
+ // Tool results become separate `role: 'tool'` messages
174
+ if (toolResults.length > 0) {
175
+ for (const tr of toolResults) {
176
+ wireMessages.push({ role: 'tool', tool_call_id: tr.id, content: tr.content });
177
+ }
178
+ continue;
179
+ }
180
+ // Build the wire message with appropriate fields
181
+ const wireMsg = { role: message.role };
182
+ wireMsg.content = textParts.length > 0 ? textParts.join('') : (toolCalls.length > 0 ? null : '');
183
+ if (toolCalls.length > 0) {
184
+ wireMsg.tool_calls = toolCalls;
185
+ }
186
+ wireMessages.push(wireMsg);
187
+ }
188
+ // §7.106 (2026-10-23) pairing guard: every `role:'tool'` message must
189
+ // reference an assistant tool_call that EXISTS IN THIS BODY. Strict
190
+ // gateways (minimax 2013 "tool result's tool id() not found") enforce the
191
+ // referential integrity DeepSeek silently tolerates. Broken histories come
192
+ // from the ollama-era models' empty-id tool calls (§7.105 class): a result
193
+ // whose call was pruned out of the window, or whose FIFO-synthesized id
194
+ // was never claimed by an assistant message, is an orphan no upstream will
195
+ // accept. Demote orphans to plain user text — the tool OUTPUT survives for
196
+ // the model to read, only the dangling reference goes away.
197
+ {
198
+ const claimedCallIds = new Set();
199
+ for (const m of wireMessages) {
200
+ const tcs = m.tool_calls;
201
+ if (Array.isArray(tcs)) {
202
+ for (const tc of tcs) {
203
+ if (typeof tc.id === 'string' && tc.id.length > 0)
204
+ claimedCallIds.add(tc.id);
205
+ }
206
+ }
207
+ }
208
+ for (let i = 0; i < wireMessages.length; i++) {
209
+ const m = wireMessages[i];
210
+ if (m.role !== 'tool')
211
+ continue;
212
+ const id = typeof m.tool_call_id === 'string' ? m.tool_call_id : '';
213
+ if (id.length > 0 && claimedCallIds.has(id))
214
+ continue;
215
+ const text = typeof m.content === 'string' ? m.content : '';
216
+ wireMessages[i] = {
217
+ role: 'user',
218
+ content: `[tool-result ${id.length > 0 ? id : '(unreferenced)'} lost its matching call; output:]\n${text}`,
219
+ };
220
+ }
221
+ }
222
+ const body = {
223
+ // CRITICAL: use the adapter's configured `defaultModel` (the real upstream
224
+ // model name, e.g. "deepseek-chat"), NOT `options.model` which is the Cordis
225
+ // provider route ID (e.g. "cloud-llm"). The route ID is meaningless
226
+ // to the upstream API — only the adapter knows its own model name.
227
+ model: defaultModel,
228
+ messages: wireMessages,
229
+ stream: true,
230
+ };
231
+ if (options.tools !== undefined && options.tools.length > 0) {
232
+ body.tools = options.tools.map((tool) => ({
233
+ type: 'function',
234
+ function: {
235
+ name: tool.name,
236
+ description: tool.description,
237
+ parameters: tool.parameters,
238
+ },
239
+ }));
240
+ }
241
+ if (temperature !== undefined)
242
+ body.temperature = temperature;
243
+ if (options.maxTokens !== undefined)
244
+ body.max_tokens = options.maxTokens;
245
+ // §7.87.5 (2026-09-19): disable chain-of-thought for thinking-capable models
246
+ // (qwen3.5 / QwQ / DeepSeek-R1). Without this, max_tokens gets burned on
247
+ // reasoning and `content` returns "" (finish_reason="length"). Pass-through
248
+ // is a no-op for non-thinking servers — they ignore unknown kwargs.
249
+ if (!enableThinking) {
250
+ body.chat_template_kwargs = { enable_thinking: false };
251
+ if (alsoReasoningEffort)
252
+ body.reasoning_effort = 'none';
253
+ }
254
+ return body;
255
+ }
256
+ /** Pull the text out of a content-block array into one concatenated string. */
257
+ export function extractText(blocks) {
258
+ return blocks
259
+ .filter((block) => block.type === 'text' && typeof block.text === 'string')
260
+ .map((block) => block.text)
261
+ .join('');
262
+ }
263
+ /** Read a `Response` body as an SSE stream and yield finished frames. */
264
+ export async function* iterateSseFrames(response, onError) {
265
+ if (!response.ok || response.body === null) {
266
+ const text = await response.text().catch(() => '');
267
+ throw new LlmError(`OpenAI-compatible SSE: HTTP ${response.status} from ${response.url}: ${text.slice(0, 512)}`, 'PROVIDER_ERROR');
268
+ }
269
+ const stream = response.body;
270
+ const reader = stream.getReader();
271
+ const decoder = new TextDecoder('utf-8');
272
+ let buffer = '';
273
+ try {
274
+ for (;;) {
275
+ const { value, done } = await reader.read();
276
+ if (done)
277
+ break;
278
+ buffer += decoder.decode(value, { stream: true });
279
+ const events = splitSseEvents(buffer);
280
+ buffer = events.remainder;
281
+ for (const event of events.events) {
282
+ const chunk = parseSseEvent(event);
283
+ if (chunk !== undefined)
284
+ yield chunk;
285
+ }
286
+ }
287
+ }
288
+ catch (error) {
289
+ onError(error.message ?? String(error));
290
+ }
291
+ finally {
292
+ reader.releaseLock();
293
+ }
294
+ }
295
+ /**
296
+ * Translate a `text-delta` or `tool-call-delta` SSE event into a DSH
297
+ * `StreamChunk`. Returns `null` for `finish` events — the caller
298
+ * tracks the finish reason separately and emits the terminal `finish`
299
+ * through `terminalChunks` once the SSE stream closes.
300
+ */
301
+ export function sseEventToChunk(event, toolCalls) {
302
+ if (event.kind === 'text-delta') {
303
+ return { chunk: { type: 'text-delta', index: 0, text: event.text }, sawText: true, sawToolCall: false };
304
+ }
305
+ if (event.kind === 'tool-call-delta') {
306
+ const current = toolCalls.get(event.index) ?? { id: '', name: '', arguments: '' };
307
+ // §7.105 (2026-10-21): the upstream `id` may be absent or empty. A
308
+ // tool-call id must be a NONEMPTY string — the host's v4 session-log
309
+ // validator rejects the entire log on an empty id ("tool call id requires
310
+ // a nonempty string"), which is how one local-llm tool call made a whole
311
+ // session fail to load. Prefer the upstream id when it has one; otherwise
312
+ // synthesize a stable id for this wire index and remember it, so every
313
+ // later delta of the same call and the final `block-end` agree.
314
+ if (event.id !== undefined && event.id !== '')
315
+ current.id = event.id;
316
+ else if (current.id === '')
317
+ current.id = synthesizeToolCallId(event.index);
318
+ if (event.name !== undefined)
319
+ current.name = event.name;
320
+ current.arguments += event.argumentsDelta;
321
+ toolCalls.set(event.index, current);
322
+ const idForChunk = current.id;
323
+ // CRITICAL: dsh-session's `snapshotJsonValue` rejects chunks with `undefined`
324
+ // fields (throws "carries non-JSON-serializable data"). When DeepSeek splits
325
+ // a tool call across multiple deltas, intermediate chunks omit `name` (and
326
+ // on the very first chunk the local accumulator's `name` is still `''`).
327
+ // Strip `undefined` so the chunk survives lossless JSON validation.
328
+ // §7.58 (2026-09-16): the chunk index MUST equal the index `terminalChunks`
329
+ // later uses for the matching `block-end` — both are `wire index + 1`
330
+ // (index 0 belongs to text). They used to disagree (delta = wire index 0,
331
+ // block-end = 1-based sequential): the host `BlockAssembler` keys partials
332
+ // by index, so the delta-accumulated call and the block-end landed in two
333
+ // different blocks → one persisted assistant message with the SAME
334
+ // tool-call duplicated under a single shared id, and the next replay got
335
+ // DeepSeek 400 "Duplicate value for 'tool_call_id' … in message[N]"
336
+ // (session 24303d6a turn 2, 2026-09-15). Only text-less tool turns were
337
+ // affected — when text came first, it squatted index 0 and the colliding
338
+ // delta was harmlessly merged into the text partial.
339
+ const chunk = {
340
+ type: 'tool-call-delta',
341
+ index: event.index + 1,
342
+ id: brandCallId(idForChunk),
343
+ argumentsDelta: event.argumentsDelta,
344
+ };
345
+ if (event.name !== undefined)
346
+ chunk.name = event.name;
347
+ return {
348
+ chunk,
349
+ sawText: false,
350
+ sawToolCall: true,
351
+ };
352
+ }
353
+ return null;
354
+ }
355
+ /** Extract the finish reason out of a `finish` event. */
356
+ export function sseFinishReason(event) {
357
+ return event.kind === 'finish' ? event.finishReason : undefined;
358
+ }
359
+ /** Emit the terminal chunks: text `block-end`, sorted tool-call `block-end`s, then `finish`. */
360
+ export function* terminalChunks(sawText, assembledText, toolCalls, finishKind, sawToolCall) {
361
+ if (sawText) {
362
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: assembledText } };
363
+ }
364
+ // CRITICAL: Tool-call block-ends must use indices that don't collide with text (index 0).
365
+ // The BlockAssembler uses chunk.index to track blocks, so text and tool-calls must have
366
+ // distinct indices. §7.58: the mapping is EXACTLY `wire index + 1` — the same one
367
+ // sseEventToChunk applies to live tool-call-delta chunks — so each call's delta
368
+ // partial and its block-end land in the SAME block slot. Sequential numbering
369
+ // (`toolCallIndex++`) instead would diverge whenever wire indices have gaps and
370
+ // double the tool-calls whenever a delta partial was already open (see the
371
+ // duplicate-id incident documented at the tool-call-delta yield above).
372
+ for (const [wireIndex, call] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
373
+ yield {
374
+ type: 'block-end',
375
+ index: wireIndex + 1,
376
+ block: {
377
+ type: 'tool-call',
378
+ // §7.105 (2026-10-21): the block-end id is the one the host persists
379
+ // into `assistant/message`, so it must never be empty. `sseEventToChunk`
380
+ // already synthesized one when the upstream omitted it; this guard
381
+ // covers a map populated by any other path.
382
+ id: brandCallId((call.id !== '' ? call.id : synthesizeToolCallId(wireIndex))),
383
+ name: call.name,
384
+ arguments: call.arguments,
385
+ },
386
+ };
387
+ }
388
+ yield {
389
+ type: 'finish',
390
+ reason: finishKind === 'length'
391
+ ? { kind: 'max-tokens' }
392
+ : finishKind === 'tool-calls' || sawToolCall
393
+ ? { kind: 'tool-calls' }
394
+ : { kind: 'stop' },
395
+ };
396
+ }
397
+ export function splitSseEvents(buffer) {
398
+ const events = [];
399
+ let idx = buffer.indexOf('\n\n');
400
+ let cursor = 0;
401
+ while (idx !== -1) {
402
+ events.push(buffer.slice(cursor, idx));
403
+ cursor = idx + 2;
404
+ idx = buffer.indexOf('\n\n', cursor);
405
+ }
406
+ return { events, remainder: buffer.slice(cursor) };
407
+ }
408
+ export function parseSseEvent(raw) {
409
+ const lines = raw.split('\n');
410
+ for (const line of lines) {
411
+ if (!line.startsWith('data:'))
412
+ continue;
413
+ const payload = line.slice(5).trim();
414
+ if (payload.length === 0 || payload === '[DONE]')
415
+ continue;
416
+ let parsed;
417
+ try {
418
+ parsed = JSON.parse(payload);
419
+ }
420
+ catch {
421
+ continue;
422
+ }
423
+ const record = parsed;
424
+ const choices = record.choices;
425
+ if (!Array.isArray(choices) || choices.length === 0)
426
+ continue;
427
+ const choice = choices[0];
428
+ const delta = choice.delta;
429
+ const content = delta?.content;
430
+ if (typeof content === 'string' && content.length > 0) {
431
+ return { kind: 'text-delta', text: content };
432
+ }
433
+ const toolCalls = delta?.tool_calls;
434
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
435
+ const first = toolCalls[0];
436
+ const fn = first.function;
437
+ const index = typeof first.index === 'number' ? first.index : 0;
438
+ return {
439
+ kind: 'tool-call-delta',
440
+ index,
441
+ id: typeof first.id === 'string' ? first.id : undefined,
442
+ name: typeof fn?.name === 'string' ? fn.name : undefined,
443
+ argumentsDelta: typeof fn?.arguments === 'string' ? fn.arguments : '',
444
+ };
445
+ }
446
+ const finishReason = choice.finish_reason;
447
+ if (typeof finishReason === 'string') {
448
+ if (finishReason === 'tool_calls') {
449
+ return { kind: 'finish', finishReason: 'tool-calls' };
450
+ }
451
+ return {
452
+ kind: 'finish',
453
+ finishReason: finishReason === 'length' ? 'length' : 'stop',
454
+ };
455
+ }
456
+ }
457
+ return undefined;
458
+ }
459
+ /** Default attribution headers every request should carry. */
460
+ export function defaultAttributionHeaders() {
461
+ return attributionHeaders();
462
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * pii-utils — 共享的 PII 检测与脱敏工具
3
+ *
4
+ * 本模块是**全项目 PII 正则的唯一事实源**(`PII_PATTERNS`)。
5
+ * 消费方:
6
+ * - `privacy-policy.ts` 的 `DEFAULT_POLICY.sanitizationRules`(直接复用
7
+ * `PII_PATTERNS`,不再各写一份字面量)
8
+ * - `sanitizer-pipeline.ts` 经由 `privacyPolicy.getSanitizationRules()`
9
+ * 消费 pattern 字符串做**可逆**脱敏
10
+ * - `prompt-guard` / `audit-log` / `local-storage` 经由本模块的
11
+ * `redactString` / `redactObject` 做**不可逆**掩码
12
+ *
13
+ * 两条链路共用同一批 pattern,只有替换语义不同(占位符 vs 掩码),
14
+ * 因此 pattern 必须集中定义,禁止在别处重复书写。
15
+ *
16
+ * @module dsh-edge-agent/pii-utils
17
+ */
18
+ export type PIIName = 'id-card' | 'bank-card' | 'phone' | 'email' | 'api-key' | 'ip-address' | 'mac-address' | 'passport' | 'license-plate' | 'credential';
19
+ /**
20
+ * PII 正则字面量(唯一事实源)。
21
+ *
22
+ * 以 `RegExp` source 语法书写(不含分隔符与 flags),便于同时用于
23
+ * `new RegExp(pattern, 'gi')`(脱敏管线)与本模块的 `RegExp` 实例。
24
+ *
25
+ * 规则顺序见 `PII_ORDER` —— 顺序是语义的一部分,长规则必须先跑。
26
+ */
27
+ export declare const PII_PATTERNS: Record<PIIName, string>;
28
+ /**
29
+ * 规则求值顺序。长规则优先(id-card → bank-card → phone),防止短规则
30
+ * 在更长的数字串内部误命中。
31
+ *
32
+ * 注意:顺序生效的前提是 `redactString` / `sanitize` 采用**累进匹配**
33
+ * (每条规则在上一条的产出上继续匹配),否则顺序没有意义。
34
+ */
35
+ export declare const PII_ORDER: PIIName[];
36
+ /** 各 PII 类型的数据分级,与 privacy-policy 的 dataClassifications 对应。 */
37
+ export declare const PII_DATA_CLASS: Record<PIIName, 'confidential' | 'secret'>;
38
+ /** 不可逆掩码函数(仅用于日志/存储,不参与上云脱敏)。 */
39
+ export declare const PII_MASKS: Record<PIIName, (match: string) => string>;
40
+ export interface PIIRule {
41
+ name: PIIName;
42
+ pattern: RegExp;
43
+ mask: (match: string) => string;
44
+ }
45
+ /**
46
+ * 10 种 PII 规则,由 `PII_PATTERNS` + `PII_MASKS` 按 `PII_ORDER` 组装。
47
+ * 与 privacy-policy 的 sanitizationRules 共用同一批 pattern 字面量。
48
+ */
49
+ export declare const PII_RULES: PIIRule[];
50
+ /**
51
+ * 对单个字符串应用所有 PII 规则,返回脱敏后的字符串和脱敏明细。
52
+ *
53
+ * 累进匹配:每条规则在**上一条规则的产出**(`result`)上继续匹配,而不是
54
+ * 回到原始 `text`。否则后序规则会命中前序规则已经掩码掉的区间,导致
55
+ * `result.replace` 静默 no-op、而 `matches` 里却记了一笔不存在的脱敏
56
+ * (2026-08-30 修复)。
57
+ *
58
+ * 同一取值在文本中出现多次时一次性全部替换,且只计一笔,避免计数虚高。
59
+ */
60
+ export declare function redactString(text: string, rules?: PIIRule[]): {
61
+ redacted: string;
62
+ matches: Array<{
63
+ type: string;
64
+ masked: string;
65
+ }>;
66
+ };
67
+ /**
68
+ * 深度遍历对象,对所有字符串值应用 PII 脱敏。
69
+ * 返回新对象(不修改原对象)。
70
+ */
71
+ export declare function redactObject<T>(obj: T, rules?: PIIRule[]): {
72
+ redacted: T;
73
+ count: number;
74
+ };
75
+ export declare function normalizeForPII(text: string): {
76
+ normalized: string;
77
+ /** `charMap[i]` = 规范化后第 i 个字符在原文本 `text` 中的源位置。
78
+ * 严格单调递增(规范化要么 1:0 删,要么 1:1 改,从不插入字符)。
79
+ * 长度 = normalized.length。 */
80
+ charMap: number[];
81
+ /** 规范化是否实际改变了文本(便于审计/forensics)。 */
82
+ changed: boolean;
83
+ };
84
+ /**
85
+ * 把 `normalizeForPII()` 返回的 charMap + normalized 上的命中区间
86
+ * `[start, end)` 翻回 `text` 上的对应区间。
87
+ *
88
+ * 用法:`sliceOriginal(text, charMap, start, end)` → 用户原始子串
89
+ * (含被丢弃的空白、被转换的中文数字)。
90
+ *
91
+ * 注意:返回字符串的字符数可能 > (end - start),因为 whitespace
92
+ * 不在 charMap 里但仍在原文本里。
93
+ */
94
+ export declare function sliceOriginal(text: string, charMap: number[], start: number, end: number): string;
95
+ export declare function containsCredential(text: string): boolean;
96
+ //# sourceMappingURL=pii-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pii-utils.d.ts","sourceRoot":"","sources":["../../src/dsh-adapter/pii-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,OAAO,GACf,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GACvD,YAAY,GAAG,aAAa,GAAG,UAAU,GAAG,eAAe,GAAG,YAAY,CAAA;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CA8BhD,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,EAW9B,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,QAAQ,CAWrE,CAAA;AAED,iCAAiC;AACjC,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAwBhE,CAAA;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAChC;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,EAI5B,CAAA;AAEH;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,OAAO,EAAc,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAwBxE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,GAAG,EAAE,CAAC,EACN,KAAK,GAAE,OAAO,EAAc,GAC3B;IAAE,QAAQ,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAuBhC;AAsED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB;;kCAE8B;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAA;CACjB,CAmDA;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EAAE,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CASR;AAUD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD"}