dsh-data-domain-knowledge 0.1.1-alpha.2 → 0.1.1-alpha.4

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 (252) hide show
  1. package/README.md +33 -16
  2. package/lib/client.js +1042 -2097
  3. package/lib/contract-refs.d.ts +0 -1
  4. package/lib/doc-page.js +1 -1
  5. package/lib/pages.js +2489 -1060
  6. package/lib/types/agent-behavior-rules.d.ts.map +1 -1
  7. package/lib/types/agent-behavior-rules.js +3 -2
  8. package/lib/types/agent-behavior-rules.js.map +1 -1
  9. package/lib/types/axis-contract.js +1 -1
  10. package/lib/types/axis-contract.js.map +1 -1
  11. package/lib/types/card-organize.d.ts +2 -1
  12. package/lib/types/card-organize.d.ts.map +1 -1
  13. package/lib/types/card-organize.js +5 -3
  14. package/lib/types/card-organize.js.map +1 -1
  15. package/lib/types/config-schema.d.ts +1 -1
  16. package/lib/types/config-schema.js +1 -1
  17. package/lib/types/convert/host.d.ts +2 -3
  18. package/lib/types/convert/host.d.ts.map +1 -1
  19. package/lib/types/convert/host.js +11 -2
  20. package/lib/types/convert/host.js.map +1 -1
  21. package/lib/types/convert/ingest.d.ts +14 -20
  22. package/lib/types/convert/ingest.d.ts.map +1 -1
  23. package/lib/types/convert/ingest.js +8 -5
  24. package/lib/types/convert/ingest.js.map +1 -1
  25. package/lib/types/diagnosis/store.d.ts +2 -2
  26. package/lib/types/diagnosis/store.d.ts.map +1 -1
  27. package/lib/types/diagnosis/store.js +10 -2
  28. package/lib/types/diagnosis/store.js.map +1 -1
  29. package/lib/types/diagnosis/types.d.ts +1 -1
  30. package/lib/types/diagnosis/types.js +1 -1
  31. package/lib/types/extract-model.d.ts +3 -0
  32. package/lib/types/extract-model.d.ts.map +1 -1
  33. package/lib/types/extract-model.js.map +1 -1
  34. package/lib/types/images.js +1 -1
  35. package/lib/types/index.d.ts +0 -4
  36. package/lib/types/index.d.ts.map +1 -1
  37. package/lib/types/index.js +99 -91
  38. package/lib/types/index.js.map +1 -1
  39. package/lib/types/knowledge-bases.d.ts +3 -0
  40. package/lib/types/knowledge-bases.d.ts.map +1 -1
  41. package/lib/types/knowledge-bases.js +22 -2
  42. package/lib/types/knowledge-bases.js.map +1 -1
  43. package/lib/types/knowledge-contract.d.ts +6 -0
  44. package/lib/types/knowledge-contract.d.ts.map +1 -1
  45. package/lib/types/knowledge-contract.js +6 -2
  46. package/lib/types/knowledge-contract.js.map +1 -1
  47. package/lib/types/layout.d.ts +9 -26
  48. package/lib/types/layout.d.ts.map +1 -1
  49. package/lib/types/layout.js +10 -28
  50. package/lib/types/layout.js.map +1 -1
  51. package/lib/types/materialize-snapshot.d.ts +5 -0
  52. package/lib/types/materialize-snapshot.d.ts.map +1 -1
  53. package/lib/types/migrate.d.ts +3 -1
  54. package/lib/types/migrate.d.ts.map +1 -1
  55. package/lib/types/migrate.js +3 -1
  56. package/lib/types/migrate.js.map +1 -1
  57. package/lib/types/model-text.d.ts +7 -0
  58. package/lib/types/model-text.d.ts.map +1 -1
  59. package/lib/types/model-text.js +31 -9
  60. package/lib/types/model-text.js.map +1 -1
  61. package/lib/types/organize-contract.d.ts.map +1 -1
  62. package/lib/types/organize-contract.js +2 -1
  63. package/lib/types/organize-contract.js.map +1 -1
  64. package/lib/types/pipeline/engine.d.ts +28 -4
  65. package/lib/types/pipeline/engine.d.ts.map +1 -1
  66. package/lib/types/pipeline/engine.js +68 -15
  67. package/lib/types/pipeline/engine.js.map +1 -1
  68. package/lib/types/pipeline/progress.d.ts +12 -0
  69. package/lib/types/pipeline/progress.d.ts.map +1 -0
  70. package/lib/types/pipeline/progress.js +37 -0
  71. package/lib/types/pipeline/progress.js.map +1 -0
  72. package/lib/types/pipeline/resumable-extract.d.ts +45 -0
  73. package/lib/types/pipeline/resumable-extract.d.ts.map +1 -0
  74. package/lib/types/pipeline/resumable-extract.js +262 -0
  75. package/lib/types/pipeline/resumable-extract.js.map +1 -0
  76. package/lib/types/pipeline/store.d.ts +8 -2
  77. package/lib/types/pipeline/store.d.ts.map +1 -1
  78. package/lib/types/pipeline/store.js +27 -5
  79. package/lib/types/pipeline/store.js.map +1 -1
  80. package/lib/types/pipeline/types.d.ts +56 -0
  81. package/lib/types/pipeline/types.d.ts.map +1 -1
  82. package/lib/types/pipeline/types.js +18 -1
  83. package/lib/types/pipeline/types.js.map +1 -1
  84. package/lib/types/present.d.ts +1 -1
  85. package/lib/types/present.js +1 -1
  86. package/lib/types/preset-ids.d.ts +0 -1
  87. package/lib/types/preset-ids.d.ts.map +1 -1
  88. package/lib/types/preset-ids.js +0 -3
  89. package/lib/types/preset-ids.js.map +1 -1
  90. package/lib/types/query-analysis.d.ts +0 -5
  91. package/lib/types/query-analysis.d.ts.map +1 -1
  92. package/lib/types/query-analysis.js +0 -16
  93. package/lib/types/query-analysis.js.map +1 -1
  94. package/lib/types/recall-delivery.d.ts +36 -0
  95. package/lib/types/recall-delivery.d.ts.map +1 -0
  96. package/lib/types/recall-delivery.js +16 -0
  97. package/lib/types/recall-delivery.js.map +1 -0
  98. package/lib/types/retrieval/scope.d.ts +17 -0
  99. package/lib/types/retrieval/scope.d.ts.map +1 -0
  100. package/lib/types/retrieval/scope.js +11 -0
  101. package/lib/types/retrieval/scope.js.map +1 -0
  102. package/lib/types/runtime.d.ts +75 -14
  103. package/lib/types/runtime.d.ts.map +1 -1
  104. package/lib/types/runtime.js +454 -198
  105. package/lib/types/runtime.js.map +1 -1
  106. package/lib/types/settings-volatile.d.ts +0 -7
  107. package/lib/types/settings-volatile.d.ts.map +1 -1
  108. package/lib/types/settings-volatile.js +0 -7
  109. package/lib/types/settings-volatile.js.map +1 -1
  110. package/lib/types/source-identity.d.ts +34 -0
  111. package/lib/types/source-identity.d.ts.map +1 -0
  112. package/lib/types/source-identity.js +85 -0
  113. package/lib/types/source-identity.js.map +1 -0
  114. package/lib/types/source-read.d.ts +18 -0
  115. package/lib/types/source-read.d.ts.map +1 -0
  116. package/lib/types/source-read.js +49 -0
  117. package/lib/types/source-read.js.map +1 -0
  118. package/lib/types/store/axis-references.d.ts.map +1 -1
  119. package/lib/types/store/axis-references.js +5 -3
  120. package/lib/types/store/axis-references.js.map +1 -1
  121. package/lib/types/store/knowledge-db.d.ts +5 -29
  122. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  123. package/lib/types/store/knowledge-db.js +57 -2
  124. package/lib/types/store/knowledge-db.js.map +1 -1
  125. package/lib/types/store/source-identity.d.ts +8 -0
  126. package/lib/types/store/source-identity.d.ts.map +1 -0
  127. package/lib/types/store/source-identity.js +56 -0
  128. package/lib/types/store/source-identity.js.map +1 -0
  129. package/lib/types/types.d.ts +46 -5
  130. package/lib/types/types.d.ts.map +1 -1
  131. package/lib/types/types.js +6 -3
  132. package/lib/types/types.js.map +1 -1
  133. package/lib/types/wiki-availability.d.ts +11 -0
  134. package/lib/types/wiki-availability.d.ts.map +1 -0
  135. package/lib/types/wiki-availability.js +39 -0
  136. package/lib/types/wiki-availability.js.map +1 -0
  137. package/lib/types/wiki-finalize.d.ts +4 -2
  138. package/lib/types/wiki-finalize.d.ts.map +1 -1
  139. package/lib/types/wiki-finalize.js +9 -27
  140. package/lib/types/wiki-finalize.js.map +1 -1
  141. package/lib/types/wiki-links.d.ts +35 -0
  142. package/lib/types/wiki-links.d.ts.map +1 -0
  143. package/lib/types/wiki-links.js +129 -0
  144. package/lib/types/wiki-links.js.map +1 -0
  145. package/lib/types/wiki-trial-types.d.ts +3 -0
  146. package/lib/types/wiki-trial-types.d.ts.map +1 -1
  147. package/lib/types/wiki-trial.d.ts +3 -3
  148. package/lib/types/wiki-trial.d.ts.map +1 -1
  149. package/lib/types/wiki-trial.js +14 -3
  150. package/lib/types/wiki-trial.js.map +1 -1
  151. package/package.json +16 -16
  152. package/scripts/doctor.mjs +0 -7
  153. package/scripts/setup-embedding.sh +0 -0
  154. package/src/agent-behavior-rules.ts +3 -2
  155. package/src/axis-contract.ts +1 -1
  156. package/src/card-organize.ts +5 -3
  157. package/src/client/AttributionEventGuide.tsx +3 -3
  158. package/src/client/AxisMatrix.tsx +86 -19
  159. package/src/client/AxisRegistrySection.tsx +1 -1
  160. package/src/client/CompileSettingsView.tsx +473 -0
  161. package/src/client/DirectoryPicker.tsx +0 -2
  162. package/src/client/ExtractionTasksPage.tsx +557 -423
  163. package/src/client/GuideArchitectureCanvas.tsx +5 -12
  164. package/src/client/ImportFilesDialog.tsx +1 -0
  165. package/src/client/InfoHint.tsx +1 -1
  166. package/src/client/KnowledgeBaseTab.tsx +141 -148
  167. package/src/client/KnowledgePanel.tsx +16 -9
  168. package/src/client/OperationLogTab.tsx +1 -1
  169. package/src/client/PipelineRunView.tsx +173 -0
  170. package/src/client/RecallPanel.tsx +5 -16
  171. package/src/client/TabGuide.tsx +3 -7
  172. package/src/client/WikiFlowPanel.tsx +10 -11
  173. package/src/client/arch/NodeLogicPanel.tsx +3 -3
  174. package/src/client/arch/model.ts +5 -113
  175. package/src/client/architecture.css +1 -1
  176. package/src/client/architecture.tsx +13 -27
  177. package/src/client/attribution/AttributionTab.tsx +141 -10
  178. package/src/client/pages.css +305 -180
  179. package/src/client/pages.tsx +7 -12
  180. package/src/client/styles.css +15 -96
  181. package/src/config-schema.ts +1 -1
  182. package/src/convert/host.ts +13 -5
  183. package/src/convert/ingest.ts +20 -7
  184. package/src/diagnosis/store.ts +7 -2
  185. package/src/diagnosis/types.ts +1 -1
  186. package/src/extract-model.ts +3 -0
  187. package/src/images.ts +1 -1
  188. package/src/index.ts +92 -97
  189. package/src/knowledge-bases.ts +23 -3
  190. package/src/knowledge-contract.ts +7 -2
  191. package/src/layout.ts +10 -32
  192. package/src/materialize-snapshot.ts +1 -0
  193. package/src/migrate.ts +3 -1
  194. package/src/model-text.ts +33 -11
  195. package/src/organize-contract.ts +2 -1
  196. package/src/pipeline/engine.ts +85 -22
  197. package/src/pipeline/progress.ts +40 -0
  198. package/src/pipeline/resumable-extract.ts +233 -0
  199. package/src/pipeline/store.ts +28 -6
  200. package/src/pipeline/types.ts +47 -2
  201. package/src/present.ts +1 -1
  202. package/src/preset-ids.ts +0 -4
  203. package/src/query-analysis.ts +0 -20
  204. package/src/recall-delivery.ts +19 -0
  205. package/src/retrieval/scope.ts +10 -0
  206. package/src/runtime.ts +412 -206
  207. package/src/settings-volatile.ts +0 -7
  208. package/src/source-identity.ts +85 -0
  209. package/src/source-read.ts +47 -0
  210. package/src/store/axis-references.ts +5 -3
  211. package/src/store/knowledge-db.ts +54 -2
  212. package/src/store/source-identity.ts +42 -0
  213. package/src/types.ts +18 -5
  214. package/src/wiki-availability.ts +37 -0
  215. package/src/wiki-finalize.ts +9 -25
  216. package/src/wiki-links.ts +120 -0
  217. package/src/wiki-trial-types.ts +3 -2
  218. package/src/wiki-trial.ts +17 -6
  219. package/lib/types/intent/engine.d.ts +0 -122
  220. package/lib/types/intent/engine.d.ts.map +0 -1
  221. package/lib/types/intent/engine.js +0 -488
  222. package/lib/types/intent/engine.js.map +0 -1
  223. package/lib/types/intent/host.d.ts +0 -68
  224. package/lib/types/intent/host.d.ts.map +0 -1
  225. package/lib/types/intent/host.js +0 -249
  226. package/lib/types/intent/host.js.map +0 -1
  227. package/lib/types/intent/scope.d.ts +0 -25
  228. package/lib/types/intent/scope.d.ts.map +0 -1
  229. package/lib/types/intent/scope.js +0 -116
  230. package/lib/types/intent/scope.js.map +0 -1
  231. package/lib/types/intent/store.d.ts +0 -33
  232. package/lib/types/intent/store.d.ts.map +0 -1
  233. package/lib/types/intent/store.js +0 -239
  234. package/lib/types/intent/store.js.map +0 -1
  235. package/lib/types/intent/tools.d.ts +0 -62
  236. package/lib/types/intent/tools.d.ts.map +0 -1
  237. package/lib/types/intent/tools.js +0 -130
  238. package/lib/types/intent/tools.js.map +0 -1
  239. package/lib/types/intent/types.d.ts +0 -170
  240. package/lib/types/intent/types.d.ts.map +0 -1
  241. package/lib/types/intent/types.js +0 -56
  242. package/lib/types/intent/types.js.map +0 -1
  243. package/src/client/IntentLogic.tsx +0 -192
  244. package/src/client/IntentTab.tsx +0 -343
  245. package/src/client/TypeDefinitionDrawer.tsx +0 -195
  246. package/src/intent/README.md +0 -42
  247. package/src/intent/engine.ts +0 -515
  248. package/src/intent/host.ts +0 -315
  249. package/src/intent/scope.ts +0 -125
  250. package/src/intent/store.ts +0 -218
  251. package/src/intent/tools.ts +0 -154
  252. package/src/intent/types.ts +0 -229
@@ -1,515 +0,0 @@
1
- import {
2
- DEFAULT_ACTS,
3
- DEFAULT_CATEGORIES,
4
- DEFAULT_INTENT_SETTINGS,
5
- DEFAULT_PREDICATES,
6
- type IntentRecord,
7
- type IntentRecordInput,
8
- type IntentRef,
9
- type IntentRelation,
10
- type IntentResult,
11
- type IntentSettings,
12
- type IntentSlotMap,
13
- type IntentSource,
14
- type IntentStoreView,
15
- type IntentTaskSummary,
16
- type IntentTurnState,
17
- } from './types.js'
18
- import { envelopeSuccess } from '#contracts'
19
- import type { LineageContext } from '#contracts'
20
-
21
- function id(prefix = 'intent'): string {
22
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
23
- }
24
-
25
- function clamp01(value: unknown): number | undefined {
26
- if (typeof value !== 'number' || !Number.isFinite(value)) return undefined
27
- return Math.max(0, Math.min(1, value))
28
- }
29
-
30
- function slotsOf(value: unknown): IntentSlotMap {
31
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
32
- return Object.fromEntries(Object.entries(value as Record<string, unknown>).filter(([, item]) => item !== null && item !== undefined))
33
- }
34
-
35
- function refsOf(value: unknown): IntentRef[] {
36
- if (!Array.isArray(value)) return []
37
- return value.flatMap((ref): IntentRef[] => {
38
- if (typeof ref === 'string' && ref.trim()) return [{ id: ref.trim() }]
39
- if (ref && typeof ref === 'object' && typeof (ref as { id?: unknown }).id === 'string') {
40
- const item = ref as { id: string; kind?: unknown; label?: unknown }
41
- return [{ id: item.id, ...(typeof item.kind === 'string' ? { kind: item.kind } : {}), ...(typeof item.label === 'string' ? { label: item.label } : {}) }]
42
- }
43
- return []
44
- })
45
- }
46
-
47
- function safeLineage(value: unknown): Partial<LineageContext> {
48
- if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
49
- const record = value as Record<string, unknown>
50
- const result: Partial<LineageContext> = {}
51
- for (const key of ['sessionId', 'turnId', 'intentId', 'taskId', 'subtaskId', 'parentSubtaskId', 'runId', 'traceId', 'domain'] as const) {
52
- if (typeof record[key] === 'string' && record[key].trim()) result[key] = record[key].trim().slice(0, 160)
53
- }
54
- return result
55
- }
56
-
57
- /** 标量字段的同类兜底:类型不对就退回默认值(引用对象、`undefined`、字符串都算不对)。 */
58
- function numberOr(value: unknown, fallback: number): number {
59
- return typeof value === 'number' && Number.isFinite(value) ? value : fallback
60
- }
61
-
62
- function booleanOr(value: unknown, fallback: boolean): boolean {
63
- return typeof value === 'boolean' ? value : fallback
64
- }
65
-
66
- function normalizeSettings(settings: IntentSettings): IntentSettings {
67
- return {
68
- ...settings,
69
- clarifyThreshold: numberOr(settings.clarifyThreshold, DEFAULT_INTENT_SETTINGS.clarifyThreshold),
70
- ttlSeconds: numberOr(settings.ttlSeconds, DEFAULT_INTENT_SETTINGS.ttlSeconds),
71
- fallbackEnabled: booleanOr(settings.fallbackEnabled, DEFAULT_INTENT_SETTINGS.fallbackEnabled),
72
- }
73
- }
74
-
75
- /**
76
- * Bounded audit store: one record per session turn, expiring after the configured TTL.
77
- *
78
- * 0033 起它同时是**工作集**与**留痕的入口**,两者的分工要说清:
79
- *
80
- * - 内存里这份是有 TTL 的**工作集**(默认 600 秒):血缘判定只关心最近的轮次,
81
- * 一个十轮之前就结束的会话不该让新的一轮接到它的 task 上。
82
- * - 磁盘上那份(`intent/intent.db`)是**留痕**,当前版本便于读取,历史版本只追加、不随 TTL 消失。
83
- * `sink` 在每次 `save` 时被调用,`hydrate` 在启动时把留痕读回工作集 ——
84
- * 重启之后同一个会话的血缘因此还在(在 TTL 窗口内)。
85
- *
86
- * 为什么不干脆让 `records()` 直接读文件:那会让 TTL 名存实亡(文件永远有),
87
- * 于是「工作集」这个概念消失,血缘会接到很久以前的 task 上。两件事分开,
88
- * 各自的契约才都成立。
89
- */
90
- export class IntentStore {
91
- private readonly sessions = new Map<string, { records: IntentRecord[]; touched: number }>()
92
- /**
93
- * 每条被保存的记录都会经过它(落盘留痕)。
94
- *
95
- * 同步签名、由调用方自己吞异常:`save` 在记录路径的关键路径上,而留痕失败
96
- * 不该让这一轮的意图记录失败 —— 「记录不拦截请求」这条契约对留痕也成立。
97
- */
98
- private sink?: (record: IntentRecord) => void
99
- constructor(private ttlSeconds = DEFAULT_INTENT_SETTINGS.ttlSeconds, private readonly now = () => Date.now()) {}
100
-
101
- /** 装上一个留痕出口。多次调用以最后一次为准。 */
102
- setSink(sink: (record: IntentRecord) => void): void {
103
- this.sink = sink
104
- }
105
-
106
- /**
107
- * 把留痕读回工作集(启动时调用一次)。
108
- *
109
- * `touched` 用记录自己的 `updatedAt`,于是「重启」不会把 TTL 归零:一份十小时
110
- * 前的记录在 TTL 600 秒下本来就该过期,重启不该让它复活。
111
- */
112
- hydrate(records: IntentRecord[]): number {
113
- let loaded = 0
114
- for (const record of records) {
115
- const item = this.sessions.get(record.sessionKey) ?? { records: [], touched: 0 }
116
- const index = item.records.findIndex(existing => existing.turnKey === record.turnKey)
117
- if (index >= 0) item.records[index] = record
118
- else item.records.push(record)
119
- item.records = item.records.slice(-100)
120
- const at = Date.parse(String(record.updatedAt ?? record.createdAt))
121
- item.touched = Math.max(item.touched, Number.isFinite(at) ? at : this.now())
122
- this.sessions.set(record.sessionKey, item)
123
- loaded += 1
124
- }
125
- return loaded
126
- }
127
-
128
- setTtl(seconds: number): void {
129
- this.ttlSeconds = Math.max(1, Math.min(86400, Math.round(seconds)))
130
- }
131
-
132
- private entry(sessionKey: string) {
133
- const current = this.sessions.get(sessionKey)
134
- if (!current || this.now() - current.touched > this.ttlSeconds * 1000) {
135
- const fresh: { records: IntentRecord[]; touched: number } = { records: [], touched: this.now() }
136
- this.sessions.set(sessionKey, fresh)
137
- return fresh
138
- }
139
- current.touched = this.now()
140
- return current
141
- }
142
-
143
- /** Upsert by turn key so repeated records within one turn stay a single fact. */
144
- save(record: IntentRecord): IntentRecord {
145
- const item = this.entry(record.sessionKey)
146
- const index = item.records.findIndex(existing => existing.turnKey === record.turnKey)
147
- if (index >= 0) item.records[index] = record
148
- else item.records.push(record)
149
- item.records = item.records.slice(-100)
150
- try { this.sink?.(record) } catch { /* 留痕失败不影响这一轮 */ }
151
- return record
152
- }
153
-
154
- records(sessionKey: string): IntentRecord[] {
155
- return this.entry(sessionKey).records
156
- }
157
-
158
- last(sessionKey: string): IntentRecord | undefined {
159
- return this.entry(sessionKey).records.at(-1)
160
- }
161
-
162
- findByTurn(sessionKey: string, turnKey: string): IntentRecord | undefined {
163
- return this.entry(sessionKey).records.find(record => record.turnKey === turnKey)
164
- }
165
-
166
- taskExists(sessionKey: string, taskId: string): boolean {
167
- return this.entry(sessionKey).records.some(record => record.lineage.taskId === taskId)
168
- }
169
-
170
- lastSubtaskOf(sessionKey: string, taskId: string): string | undefined {
171
- return this.entry(sessionKey).records.filter(record => record.lineage.taskId === taskId).at(-1)?.lineage.subtaskId
172
- }
173
-
174
- expiresAt(sessionKey: string): string | undefined {
175
- const item = this.entry(sessionKey)
176
- return item.records.length > 0 ? new Date(item.touched + this.ttlSeconds * 1000).toISOString() : undefined
177
- }
178
-
179
- clear(sessionKey: string): void {
180
- this.sessions.delete(sessionKey)
181
- }
182
-
183
- /** When the session was last touched (ms since epoch); 0 when unknown. */
184
- touchedAt(sessionKey: string): number {
185
- return this.sessions.get(sessionKey)?.touched ?? 0
186
- }
187
-
188
- /** Live session keys, most recently touched first. Expired sessions are skipped. */
189
- sessionKeys(): string[] {
190
- const now = this.now()
191
- const live: Array<{ key: string; touched: number }> = []
192
- for (const [key, item] of this.sessions) {
193
- if (now - item.touched > this.ttlSeconds * 1000) continue
194
- live.push({ key, touched: item.touched })
195
- }
196
- return live.sort((left, right) => right.touched - left.touched).map(item => item.key)
197
- }
198
- }
199
-
200
- /**
201
- * Intent recorder. Every entry point is total: an unrecognized act, an unknown
202
- * task reference, or a missing classification degrade into warnings on a stored
203
- * record. Nothing here blocks, confirms, or rewrites the user's request.
204
- */
205
- export class IntentEngine {
206
- readonly store: IntentStore
207
- private settings: IntentSettings
208
- private readonly turns = new Map<string, IntentTurnState>()
209
-
210
- constructor(settings: Partial<IntentSettings> = {}, store?: IntentStore) {
211
- this.settings = normalizeSettings({ ...DEFAULT_INTENT_SETTINGS, ...settings })
212
- this.store = store ?? new IntentStore(this.settings.ttlSeconds)
213
- }
214
-
215
- setSettings(settings: Partial<IntentSettings>): void {
216
- this.settings = normalizeSettings({ ...this.settings, ...settings })
217
- this.store.setTtl(this.settings.ttlSeconds)
218
- }
219
-
220
- getSettings(): IntentSettings {
221
- return this.settings
222
- }
223
-
224
- /**
225
- * Open a turn and allocate its provisional ids before the model records.
226
- * Downstream calls dispatched in parallel with the record can already carry
227
- * an id; the record later commits the authoritative task binding.
228
- */
229
- beginTurn(sessionKey: string, turn: number, userInput: string, eligible = true): IntentTurnState {
230
- const state: IntentTurnState = {
231
- turn,
232
- userInput: userInput.slice(0, 2000),
233
- subtaskId: id('subtask'),
234
- taskId: this.store.last(sessionKey)?.lineage.taskId,
235
- eligible,
236
- dataToolUsed: false,
237
- committed: false,
238
- }
239
- this.turns.set(sessionKey, state)
240
- return state
241
- }
242
-
243
- /**
244
- * 用户消息可能在 `turn/start` 之后才到达。
245
- *
246
- * 宿主事件顺序不是固定的:有些会话先发 `turn/start`,再发 `user/message`。如果只在
247
- * `beginTurn` 读取一次原文,就会把整轮意图永久记录成空字符串。收到真实用户消息后,
248
- * 只更新尚未提交的当前轮次;已经记录的判断不再被后来事件改写。
249
- */
250
- updateTurnInput(sessionKey: string, userInput: string): boolean {
251
- const state = this.turns.get(sessionKey)
252
- if (!state || state.committed || !userInput.trim()) return false
253
- state.userInput = userInput.slice(0, 2000)
254
- return true
255
- }
256
-
257
- /** Mark the current turn as a data turn without classifying it. */
258
- markDataIntent(sessionKey: string, turn?: number): boolean {
259
- const state = this.turns.get(sessionKey)
260
- if (!state || state.committed || (turn !== undefined && state.turn !== turn)) return false
261
- state.eligible = true
262
- return true
263
- }
264
-
265
- /** Mark the current turn as a data turn based on actual data-tool use. */
266
- markDataToolUse(sessionKey: string, turn?: number): boolean {
267
- const state = this.turns.get(sessionKey)
268
- if (!state || state.committed || (turn !== undefined && state.turn !== turn)) return false
269
- state.eligible = true
270
- state.dataToolUsed = true
271
- return true
272
- }
273
-
274
- /** Clear a text-only gate when the model explicitly identifies a non-data turn. */
275
- clearDataIntent(sessionKey: string, turn?: number): boolean {
276
- const state = this.turns.get(sessionKey)
277
- if (!state || state.committed || (turn !== undefined && state.turn !== turn)) return false
278
- if (!state.dataToolUsed) state.eligible = false
279
- return true
280
- }
281
-
282
- /** Whether the current turn is eligible for data intent recording. */
283
- isDataIntentTurn(sessionKey: string, turn?: number): boolean {
284
- const state = this.turns.get(sessionKey)
285
- return Boolean(state && !state.committed && state.eligible && (turn === undefined || state.turn === turn))
286
- }
287
-
288
- /** Backfill one `unknown` record only when an eligible data turn has no model record. */
289
- fallbackWhenMissing(sessionKey: string, turn: number): IntentResult | undefined {
290
- if (!this.settings.fallbackEnabled) return undefined
291
- const state = this.turns.get(sessionKey)
292
- if (!state || state.turn !== turn || state.committed) return undefined
293
- if (!state.eligible) {
294
- this.turns.delete(sessionKey)
295
- return undefined
296
- }
297
- const result = this.record({
298
- userInput: state.userInput,
299
- act: 'unknown',
300
- category: 'unknown',
301
- relation: 'unknown',
302
- sessionKey,
303
- }, 'fallback')
304
- if (result.ok) {
305
- state.committed = true
306
- this.turns.delete(sessionKey)
307
- }
308
- return result
309
- }
310
-
311
- /** Record the model's intent judgement. The host applies scope before calling this method. */
312
- record(input: IntentRecordInput, source: IntentSource = 'model'): IntentResult {
313
- const sessionKey = input.sessionKey?.trim() || 'default'
314
- const timestamp = new Date(input.now ?? Date.now()).toISOString()
315
- const warnings: string[] = []
316
- const state = this.turns.get(sessionKey)
317
- const turnKey = state ? `${sessionKey}#${state.turn}` : `${sessionKey}#${input.turnId?.trim() || 'manual'}`
318
- const existing = this.store.findByTurn(sessionKey, turnKey)
319
- const lineageInput = safeLineage(input.lineage)
320
-
321
- const userInput = input.userInput?.trim() ? input.userInput : state?.userInput ?? ''
322
- const act = input.act?.trim() || 'unknown'
323
- const category = input.category?.trim() || undefined
324
- const confidence = clamp01(input.confidence)
325
- if (!userInput.trim()) warnings.push('user_input 为空,仅记录了一条空意图')
326
- if (act !== 'unknown' && !DEFAULT_ACTS.some(item => item.id === act && item.enabled)) warnings.push(`act "${act}" 不在内置 act 列表中,已按原样记录`)
327
- if (category && !DEFAULT_CATEGORIES.some(item => item.id === category && item.enabled)) warnings.push(`category "${category}" 不是内置分类,已按原样记录`)
328
- const lowConfidence = confidence !== undefined && confidence < this.settings.clarifyThreshold
329
- if (lowConfidence) warnings.push(`confidence ${confidence.toFixed(2)} 低于阈值 ${this.settings.clarifyThreshold.toFixed(2)},已标记 lowConfidence`)
330
-
331
- const requestedTaskId = lineageInput.taskId
332
- let relation: IntentRelation = input.relation ?? (requestedTaskId ? 'subtask_of' : 'new_task')
333
- let taskId: string | undefined
334
- let parentSubtaskId: string | undefined
335
- if (relation === 'subtask_of') {
336
- if (requestedTaskId && this.store.taskExists(sessionKey, requestedTaskId)) {
337
- taskId = requestedTaskId
338
- parentSubtaskId = this.store.lastSubtaskOf(sessionKey, requestedTaskId)
339
- } else {
340
- warnings.push(requestedTaskId
341
- ? `task_id "${requestedTaskId}" 在本会话不存在,本轮已按 new_task 记录`
342
- : 'relation=subtask_of 缺少 task_id,本轮已按 new_task 记录')
343
- relation = 'new_task'
344
- }
345
- }
346
- if (!taskId) {
347
- taskId = relation === 'new_task'
348
- ? id('task')
349
- : existing?.lineage.taskId ?? state?.taskId ?? id('task')
350
- }
351
- const subtaskId = existing?.lineage.subtaskId ?? state?.subtaskId ?? id('subtask')
352
- const runId = lineageInput.runId ?? existing?.lineage.runId ?? id('run')
353
- const intentId = existing?.id ?? id('intent')
354
- const turnId = input.turnId?.trim() || existing?.turnId || (state ? `turn-${state.turn}` : id('turn'))
355
-
356
- const record: IntentRecord = {
357
- id: intentId,
358
- sessionKey,
359
- turnKey,
360
- turnId,
361
- userInput,
362
- act,
363
- ...(category ? { category } : {}),
364
- ...(input.predicate?.trim() ? { predicate: input.predicate.trim() } : {}),
365
- slots: slotsOf(input.slots),
366
- ...(confidence !== undefined ? { confidence } : {}),
367
- ...(lowConfidence ? { lowConfidence: true } : {}),
368
- refs: refsOf(input.refs),
369
- relation,
370
- source,
371
- createdAt: existing?.createdAt ?? timestamp,
372
- updatedAt: timestamp,
373
- lineage: {
374
- ...lineageInput,
375
- sessionId: lineageInput.sessionId ?? sessionKey,
376
- turnId,
377
- intentId,
378
- taskId,
379
- subtaskId,
380
- ...(parentSubtaskId ? { parentSubtaskId } : {}),
381
- runId,
382
- },
383
- meta: {},
384
- }
385
- this.store.save(record)
386
- if (state) {
387
- state.committed = true
388
- state.taskId = taskId
389
- state.subtaskId = subtaskId
390
- }
391
-
392
- // Built through the shared constructor. This literal used to omit
393
- // `error: null`, which made an intent result the one envelope in the repo
394
- // that did not satisfy spec/contracts/envelope.json.
395
- return envelopeSuccess({
396
- intentId,
397
- taskId,
398
- subtaskId,
399
- ...(parentSubtaskId ? { parentSubtaskId } : {}),
400
- runId,
401
- turnId,
402
- act,
403
- ...(category ? { category } : {}),
404
- relation,
405
- source,
406
- lowConfidence,
407
- activeTasks: this.tasks(sessionKey),
408
- }, warnings)
409
- }
410
-
411
- /** Ids a downstream tool call in this session should carry. */
412
- activeLineage(sessionKey: string): { lineage: Partial<LineageContext>; committed: boolean } | undefined {
413
- const last = this.store.last(sessionKey)
414
- if (last) return { lineage: { ...last.lineage }, committed: this.turns.get(sessionKey)?.committed ?? true }
415
- const state = this.turns.get(sessionKey)
416
- if (!state) return undefined
417
- return { lineage: { sessionId: sessionKey, ...(state.taskId ? { taskId: state.taskId } : {}), subtaskId: state.subtaskId }, committed: false }
418
- }
419
-
420
- tasks(sessionKey: string): IntentTaskSummary[] {
421
- const now = Date.now()
422
- const seen = new Map<string, IntentTaskSummary>()
423
- for (const record of this.store.records(sessionKey)) {
424
- const taskId = record.lineage.taskId
425
- if (!taskId) continue
426
- seen.set(taskId, {
427
- taskId,
428
- ...(record.lineage.subtaskId ? { lastSubtaskId: record.lineage.subtaskId } : {}),
429
- ...(record.category ? { category: record.category } : {}),
430
- ageSeconds: Math.max(0, Math.round((now - Date.parse(record.updatedAt)) / 1000)),
431
- })
432
- }
433
- return [...seen.values()].slice(-3).reverse()
434
- }
435
-
436
- view(sessionKey = 'default', includePrompt = false): IntentResult {
437
- const last = this.store.last(sessionKey)
438
- const view: IntentStoreView = {
439
- sessionKey,
440
- ...(last?.lineage.taskId ? { activeTaskId: last.lineage.taskId } : {}),
441
- ...(last ? { activeIntentId: last.id } : {}),
442
- tasks: this.tasks(sessionKey),
443
- recent: this.store.records(sessionKey).slice(-10).reverse(),
444
- ...(this.store.expiresAt(sessionKey) ? { expiresAt: this.store.expiresAt(sessionKey) } : {}),
445
- }
446
- return envelopeSuccess({ store: view, ...(includePrompt ? { prompt: this.routingPrompt() } : {}) })
447
- }
448
-
449
- /**
450
- * Session-level index for the 意图 panel: every live session with its last
451
- * classification and active task ids. Records themselves stay per-session.
452
- */
453
- overview(): { sessions: Array<Record<string, unknown>> } {
454
- const sessions = this.store.sessionKeys().map(sessionKey => {
455
- const last = this.store.last(sessionKey)
456
- return {
457
- sessionKey,
458
- recordCount: this.store.records(sessionKey).length,
459
- lastAt: new Date(this.store.touchedAt(sessionKey)).toISOString(),
460
- tasks: this.tasks(sessionKey),
461
- ...(last ? { activeIntentId: last.id } : {}),
462
- ...(last?.act ? { lastAct: last.act } : {}),
463
- ...(last?.category ? { lastCategory: last.category } : {}),
464
- ...(last?.relation ? { lastRelation: last.relation } : {}),
465
- ...(last?.lineage.taskId ? { activeTaskId: last.lineage.taskId } : {}),
466
- ...(last && last.confidence !== undefined ? { lastConfidence: last.confidence } : {}),
467
- }
468
- })
469
- return { sessions }
470
- }
471
-
472
- /**
473
- * Model-facing contract: record first, then continue; the record never gates the request.
474
- *
475
- * 这段文本就是 `intent_record` 的**工具描述**(0023 起不再单独注入 system prompt 段),
476
- * 分类规则固定随工具在场,不再通过设置或注册表变更。
477
- *
478
- * 分类判定是**两段式**的(issue 0023):
479
- * 1. 先拿用户意思去比对预置分类的 `description` / `examples`,命中就**直接用该 id**;
480
- * 2. 全不命中才允许自行判断(新起一个 id 或落 `unknown`)。
481
- *
482
- * 这样预置项就是"先验",而不是一份仅供参考的清单——模型不必每轮从零分类。
483
- */
484
- routingPrompt(): string {
485
- const routes = DEFAULT_CATEGORIES
486
- .map(category => {
487
- const head = `- ${category.id}(${category.label})→ ${category.route}${category.description ? `:${category.description}` : ''}`
488
- const examples = (category.examples ?? []).filter(Boolean)
489
- return examples.length > 0 ? `${head}\n 例如:${examples.join(' / ')}` : head
490
- })
491
- .join('\n')
492
- return [
493
- '本工具支持 standard preset 的顶层会话。先判断本轮是否属于数据 / 知识域;只有属于数据域时,才调用 intent_record 落一条数据意图审计;寒暄、闲聊、Agent 自身说明、纯写作、代码和一般运维请求不要调用,也不要补 unknown。记录不改变、不阻塞、也不需要用户确认。',
494
- '如果当前请求会使用数据、知识库、表格、报告、SourceRaw、WikiCard、查询、分析、导入、编译或召回工具,它属于数据域;否则按普通对话处理。',
495
- '当前轮是新的任务用 relation=new_task;如果是在延续某个已有任务(追问、补充、纠正),用 relation=subtask_of 并带上该任务的 task_id;无法判断时省略 relation。',
496
- '',
497
- '分类判定分两步,必须先匹配预置、再自行判断:',
498
- '1) 先把用户的意思和下面的「预置分类」逐条比对——看定义,也看"例如"里的说法。命中就直接用该分类的 id,不要另起一个近义的新 id,也不要因为措辞不同就换一个分类。',
499
- '2) 所有预置分类都不命中时,才自行判断:可以按语义新起一个短语作为分类 id,或落 unknown。新起的 id 用英文小写下划线,并在 slots 里带上关键参数。',
500
- '',
501
- '预置分类(category 参数传 id):',
502
- routes,
503
- '',
504
- 'act 回答"这一轮用户在做什么",挑最贴近的一个:',
505
- DEFAULT_ACTS
506
- .map(def => `- ${def.id}${def.label && def.label !== def.id ? `(${def.label})` : ''}${def.description ? `:${def.description}` : ''}`)
507
- .join('\n'),
508
- `可用 predicate:${DEFAULT_PREDICATES.join('、')}`,
509
- ].join('\n')
510
- }
511
- }
512
-
513
- export function createIntentEngine(settings?: Partial<IntentSettings>): IntentEngine {
514
- return new IntentEngine(settings)
515
- }