dsh-plugin-prompt-tool 2.1.0 → 3.0.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.
- package/README.md +100 -233
- package/engine/actions/append-context.mjs +3 -24
- package/engine/actions/assembly.mjs +70 -94
- package/engine/actions/catalog.mjs +65 -28
- package/engine/actions/content.mjs +8 -152
- package/engine/actions/examples.mjs +8 -4
- package/engine/actions/inbox-prepend.mjs +4 -5
- package/engine/actions/index.mjs +17 -9
- package/engine/actions/inject-text.mjs +24 -4
- package/engine/actions/pre-step-filter.mjs +3 -4
- package/engine/actions/registration.mjs +20 -28
- package/engine/actions/request-params.mjs +21 -1
- package/engine/actions/shared.mjs +22 -13
- package/engine/actions/stream-output.mjs +26 -0
- package/engine/actions/tool-gate.mjs +31 -0
- package/engine/actions/tool-result.mjs +36 -0
- package/engine/actions/tool-visibility.mjs +248 -0
- package/engine/agent-capability.mjs +67 -0
- package/engine/anchor-match.mjs +14 -21
- package/engine/audience.d.mts +2 -0
- package/engine/audience.mjs +102 -0
- package/engine/branch.d.mts +4 -0
- package/engine/branch.mjs +61 -18
- package/engine/compaction-epoch.d.mts +25 -0
- package/engine/compaction-epoch.mjs +27 -4
- package/engine/compositions/source/local/filesystem-editor.yml +3 -3
- package/engine/compositions/source/local/instruction-hint.yml +2 -4
- package/engine/compositions/source/local/module-file-tools.yml +2 -0
- package/engine/compositions/source/local/module-import-tools.yml +4 -0
- package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
- package/engine/compositions/source/local/module-rule-tools.yml +5 -0
- package/engine/compositions/source/local/module-variable-tools.yml +5 -0
- package/engine/compositions/source/local/present-as.yml +26 -0
- package/engine/compositions/source/local/tool-config-engine.yml +2 -3
- package/engine/compositions/source/local/tool-search.yml +11 -0
- package/engine/condition-facts.mjs +84 -0
- package/engine/conditions/anchor.mjs +63 -23
- package/engine/conditions/availability.mjs +1 -1
- package/engine/conditions/composite.mjs +3 -3
- package/engine/conditions/count.mjs +42 -11
- package/engine/conditions/evaluation.mjs +74 -19
- package/engine/conditions/examples.mjs +2 -1
- package/engine/conditions/index.d.mts +15 -0
- package/engine/conditions/index.mjs +23 -9
- package/engine/conditions/operators.mjs +4 -0
- package/engine/conditions/phase.mjs +9 -3
- package/engine/conditions/preset.mjs +1 -1
- package/engine/conditions/session.mjs +8 -2
- package/engine/conditions/subject.mjs +144 -65
- package/engine/conditions/text.mjs +26 -8
- package/engine/conditions/variable.mjs +83 -0
- package/engine/context-activation.d.mts +11 -0
- package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
- package/engine/delivery.d.mts +2 -0
- package/engine/delivery.mjs +93 -0
- package/engine/discovery.d.mts +38 -0
- package/engine/discovery.mjs +146 -0
- package/engine/env-safety.mjs +14 -0
- package/engine/executor.d.mts +28 -0
- package/engine/executor.mjs +290 -205
- package/engine/fillers.mjs +39 -8
- package/engine/fixed-registration.mjs +12 -0
- package/engine/history.mjs +71 -0
- package/engine/instruction-blocks.d.mts +13 -0
- package/engine/instruction-blocks.mjs +21 -0
- package/engine/instruction-files.d.mts +5 -0
- package/engine/instruction-files.mjs +97 -0
- package/engine/instruction-hint.mjs +69 -103
- package/engine/interpolate.d.mts +7 -0
- package/engine/interpolate.mjs +44 -13
- package/engine/layers.mjs +163 -227
- package/engine/machine-paths.mjs +40 -0
- package/engine/module-file-tools.mjs +8 -0
- package/engine/module-import-tools.mjs +10 -0
- package/engine/module-manifest-tools.mjs +8 -0
- package/engine/module-rule-tools.mjs +10 -0
- package/engine/module-variable-tools.mjs +10 -0
- package/engine/order.d.mts +14 -0
- package/engine/order.mjs +0 -3
- package/engine/path-safety.d.mts +2 -0
- package/engine/path-safety.mjs +58 -0
- package/engine/pre-step-source.d.mts +69 -0
- package/engine/pre-step-source.mjs +103 -0
- package/engine/present-as.mjs +77 -0
- package/engine/promotion-pool.d.mts +9 -0
- package/engine/promotion-pool.mjs +69 -0
- package/engine/record.mjs +2 -0
- package/engine/rule-engine.mjs +3 -2
- package/engine/rule-facts.d.mts +11 -0
- package/engine/rule-facts.mjs +60 -0
- package/engine/rule-runtime.d.mts +46 -0
- package/engine/rule-runtime.mjs +98 -13
- package/engine/rule-spec.d.mts +106 -0
- package/engine/rule-spec.mjs +301 -27
- package/engine/run-code-env.mjs +73 -19
- package/engine/schema.mjs +165 -243
- package/engine/sdk-strip.mjs +2 -2
- package/engine/service-names.d.mts +10 -0
- package/engine/service-names.mjs +10 -0
- package/engine/shared.d.mts +21 -0
- package/engine/shared.mjs +46 -13
- package/engine/skill-search.mjs +125 -53
- package/engine/source-kind.d.mts +9 -0
- package/engine/source-kind.mjs +16 -0
- package/engine/strategy-ids.mjs +5 -0
- package/engine/template-evaluation.d.mts +16 -0
- package/engine/template-evaluation.mjs +178 -0
- package/engine/template-frame.d.mts +16 -0
- package/engine/template-frame.mjs +69 -0
- package/engine/tool-config-engine.mjs +38 -34
- package/engine/tool-definition.d.mts +4 -0
- package/engine/tool-definition.mjs +5 -1
- package/engine/tool-git-bash.mjs +5 -1
- package/engine/tool-scope.mjs +22 -0
- package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
- package/engine/trigger.mjs +15 -270
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
- package/engine/vendor/yaml/package.json +1 -1
- package/lib/client.js +8312 -6724
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2120
- package/lib/index.mjs +26914 -5816
- package/modules/ponytail/module.yml +83 -203
- package/modules/ptc-mode/module.yml +45 -0
- package/modules/soul/assets/core-hard.md +6 -0
- package/modules/soul/assets/criteria.md +12 -0
- package/modules/soul/assets/echo.md +12 -0
- package/modules/soul/assets/layers.md +11 -0
- package/modules/soul/assets/ledger.md +9 -0
- package/modules/soul/assets/manifest.yml +20 -0
- package/modules/soul/assets/reference/00-layers.md +27 -0
- package/modules/soul/assets/reference/01-criteria.md +32 -0
- package/modules/soul/assets/reference/02-echo.md +161 -0
- package/modules/soul/assets/reference/03-vital.md +183 -0
- package/modules/soul/assets/reference/04-manifest.json +106 -0
- package/modules/soul/assets/reference/05-doctor.md +67 -0
- package/modules/soul/assets/reference/06-reference.md +189 -0
- package/modules/soul/assets/reference/07-bundle.json +431 -0
- package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
- package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
- package/modules/soul/assets/reference/10-echo-anatomy.md +109 -0
- package/modules/soul/assets/reference/11-self.md +80 -0
- package/modules/soul/assets/reference/12-ledger.md +159 -0
- package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -0
- package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -0
- package/modules/soul/assets/reference/README.md +12 -0
- package/modules/soul/assets/templates/agents-template.md +6 -0
- package/modules/soul/assets/vital.md +9 -0
- package/modules/soul/module.yml +311 -0
- package/modules/superpowers/module.yml +122 -0
- package/modules/tool-surface/module.yml +51 -47
- package/package.json +37 -40
- package/skills/dsh-module/SKILL.md +67 -147
- package/skills/dsh-module/reference.md +113 -86
- package/templates/10-pre-step.yml +2 -3
- package/templates/14-first-turn-anchor.yml +10 -14
- package/templates/15-guide-auto.yml +32 -28
- package/templates/16-custom-fallback.yml +33 -21
- package/templates/20-system-section.yml +1 -4
- package/templates/50-llm-stream.yml +4 -10
- package/templates/60-tool-pipeline.yml +12 -10
- package/templates/63-append-context.yml +4 -6
- package/templates/65-turn-stop.yml +2 -2
- package/templates/67-inbox-prepend.yml +2 -2
- package/templates/70-subagent-maintenance.yml +9 -10
- package/templates/75-subagent-end.yml +2 -2
- package/templates/80-tool-surface.yml +41 -57
- package/templates/tools/40-delegate-module-import.yml +22 -0
- package/templates/tools/50-delegate-module-rules.yml +14 -0
- package/templates/tools/60-delegate-module-variables.yml +17 -0
- package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
- package/engine/actions/decision.mjs +0 -54
- package/engine/actions/guard.mjs +0 -80
- package/engine/actions/sdk-strip.mjs +0 -30
- package/engine/character-tools.mjs +0 -8
- package/engine/classify-task.mjs +0 -53
- package/engine/compositions/source/local/character-tools.yml +0 -5
- package/engine/compositions/source/local/dev-tool-search.yml +0 -11
- package/engine/compositions/source/local/session-var-tools.yml +0 -5
- package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
- package/engine/compositions/source/local/world-book-tools.yml +0 -5
- package/engine/condition.mjs +0 -96
- package/engine/declared-triggers.mjs +0 -93
- package/engine/session-var-tools.mjs +0 -8
- package/engine/session-vars.mjs +0 -54
- package/engine/st-macros.d.mts +0 -22
- package/engine/st-macros.mjs +0 -208
- package/engine/st-render.mjs +0 -109
- package/engine/st-world-book.d.mts +0 -10
- package/engine/subagent-tool-policy-core.mjs +0 -400
- package/engine/subagent-tool-policy.mjs +0 -352
- package/engine/trigger-spec.mjs +0 -173
- package/engine/world-book-tools.mjs +0 -8
- package/lib/migrate-rules.d.mts +0 -1
- package/lib/migrate-rules.mjs +0 -610
- package/lib/module-package-DqJZaE71.mjs +0 -2
- package/lib/module-package-q2gDRu9H.mjs +0 -2869
- package/lib/rule-builder-DMrJten6.mjs +0 -14767
- package/templates/61-deny-tools-presentation.yml +0 -27
- package/templates/62-guard-deny-tools.yml +0 -20
- package/templates/policies/legacy-defaults.yml +0 -34
- package/templates/tools/40-delegate-character.yml +0 -15
- package/templates/tools/50-delegate-worldbook.yml +0 -15
- package/templates/tools/60-delegate-session-var.yml +0 -15
- package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** 原生上下文激活选择器;扫描、候选池与实际投递提交共用一条运行链。 */
|
|
2
2
|
import { createAnchorMatcher } from './anchor-match.mjs'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { currentEvents, historyEvents } from './history.mjs'
|
|
4
|
+
import { evaluateTemplate } from './template-evaluation.mjs'
|
|
5
|
+
import { isConversationMessage } from './condition-facts.mjs'
|
|
6
|
+
import { isSuccessfulCompactionEnd } from './compaction-epoch.mjs'
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
export function
|
|
8
|
+
/** 只扫描真实对话的可见文本,排除插件注入与思维块(判据与 userText / 关键词 haystack 同源)。 */
|
|
9
|
+
export function conversationMessages(session, pending = []) {
|
|
8
10
|
const result = [], ids = new Set()
|
|
9
11
|
const add = (message, role) => {
|
|
10
|
-
if (!message || !['user', 'assistant'].includes(role) || message
|
|
11
|
-
|| (message.source?.kind && message.source.kind !== 'user')) return
|
|
12
|
+
if (!message || !['user', 'assistant'].includes(role) || !isConversationMessage(message)) return
|
|
12
13
|
if (message.id && ids.has(message.id)) return
|
|
13
14
|
if (message.id) ids.add(message.id)
|
|
14
15
|
const text = (Array.isArray(message.content) ? message.content : []).filter(block => block.type === 'text' || block.type === undefined).map(block => block.text ?? '').join('')
|
|
15
16
|
result.push({ id: message.id, role, text })
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
// 关键词世界书由**模型看得见的**对话触发;被压缩遮蔽的旧对话不该再持续命中条目。
|
|
19
|
+
// 位置替换后同一 seq 可在 `surface.nodes` 里占多个位置(`history.mjs` 头部):无 id 的消息
|
|
20
|
+
// 按 id 去不掉,会重复计数并抬高 `chat.length`(它驱动 delay / sticky 窗口与概率 generation)。
|
|
21
|
+
const seenSeq = new Set()
|
|
22
|
+
for (const event of currentEvents(session)) {
|
|
23
|
+
if (event?.seq !== undefined) {
|
|
24
|
+
if (seenSeq.has(event.seq)) continue
|
|
25
|
+
seenSeq.add(event.seq)
|
|
26
|
+
}
|
|
18
27
|
if (event?.type === 'user/message' || event?.type === 'assistant/message') add(event.data?.message ?? event.data, event.type.split('/')[0])
|
|
19
28
|
}
|
|
20
29
|
for (const message of pending) add(message, message.role ?? 'user')
|
|
@@ -22,6 +31,8 @@ export function stChatMessages(session, pending = []) {
|
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
const sessions = new WeakMap()
|
|
34
|
+
/** 每个会话最近一次成功压缩在日志里的位置;前推即窗口状态的重基准点。 */
|
|
35
|
+
const boundaries = new WeakMap()
|
|
25
36
|
const matchers = new WeakMap()
|
|
26
37
|
/** 每个会话只保留最近一次选择的诊断快照(随会话对象 GC 释放)。 */
|
|
27
38
|
const lastRuns = new WeakMap()
|
|
@@ -32,12 +43,27 @@ const DIAGNOSTIC_LIMIT = 200
|
|
|
32
43
|
|
|
33
44
|
/** 最近一次世界书选择诊断(只读快照;无记录时返回空集合,不触发任何求值)。
|
|
34
45
|
* `evaluated: false` 表示该会话还没求值过,与"已求值但本次为空"区分开。 */
|
|
35
|
-
export function
|
|
46
|
+
export function lastContextActivationDiagnostics(session) {
|
|
36
47
|
return lastRuns.get(session) ?? { records: [], truncated: false, step: 0, evaluated: false }
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
/**
|
|
51
|
+
* 最近一次成功压缩在日志里的位置(-1 = 从未压缩)。
|
|
52
|
+
*
|
|
53
|
+
* 窗口状态(`last` 与 delay / sticky / cooldown 的基准)记的是当时的**绝对可见计数**,
|
|
54
|
+
* 而 `chat` 现在来自 `currentEvents`(可缩小的视图):成功压缩把可见面塌成一条摘要后计数骤降,
|
|
55
|
+
* 旧基准不重则 sticky 恒真(命中早已不可见的条目)、cooldown 被静默跳过。判据与 count / anchor
|
|
56
|
+
* 的失效点同源(`isSuccessfulCompactionEnd`);本模块的选择由批首直接调用、没有 observe 接线,
|
|
57
|
+
* 所以在批首比边界。倒序查最近一次,压缩之后的日志长度即扫描代价。
|
|
58
|
+
*/
|
|
59
|
+
function lastCompactionBoundary(session) {
|
|
60
|
+
const events = historyEvents(session)
|
|
61
|
+
for (let index = events.length - 1; index >= 0; index--) if (isSuccessfulCompactionEnd(events[index])) return index
|
|
62
|
+
return -1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function selectContextActivation(configs, session, messages, warn = () => {}, readVariables = () => ({ module: {}, global: {} })) {
|
|
66
|
+
const entries = configs.filter(config => config.enabled !== false && config.strategy === 'context-activation')
|
|
41
67
|
// 本次选择的有界观测快照:`selection.diagnostics` 与会话最近快照引用**同一对象**,
|
|
42
68
|
// 因此 commit 阶段追加记录或把 truncated 置真都写回同一份事实(不复制布尔值,
|
|
43
69
|
// 否则 commit 越限时读取端仍看到 truncated=false)。
|
|
@@ -54,13 +80,20 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
|
54
80
|
}
|
|
55
81
|
// 被显式禁用的 ST 条目:由本层负责报告,不由 UI 猜测。
|
|
56
82
|
for (const config of configs) {
|
|
57
|
-
if (config.enabled === false && config.strategy === '
|
|
83
|
+
if (config.enabled === false && config.strategy === 'context-activation') note(config, 'excluded', 'disabled')
|
|
58
84
|
}
|
|
59
85
|
if (!entries.length) return finish(new Set())
|
|
60
|
-
const chat =
|
|
86
|
+
const chat = conversationMessages(session, messages)
|
|
87
|
+
const batch = conversationMessages(undefined, messages)
|
|
61
88
|
snapshot.step = chat.length
|
|
89
|
+
const boundary = lastCompactionBoundary(session)
|
|
62
90
|
let state = sessions.get(session)
|
|
63
|
-
|
|
91
|
+
// 边界前推说明可见视图被重写过(成功压缩)→ 丢弃本会话全部窗口,让每条条目按当前可见面重判。
|
|
92
|
+
if (boundaries.get(session) !== boundary) {
|
|
93
|
+
state = new WeakMap()
|
|
94
|
+
sessions.set(session, state)
|
|
95
|
+
boundaries.set(session, boundary)
|
|
96
|
+
}
|
|
64
97
|
const generation = JSON.stringify([chat.length, chat.at(-1)])
|
|
65
98
|
const selected = new Set(), failed = new Set(), occupied = new Set(), stickyEntries = new Set(), candidates = []
|
|
66
99
|
const updates = new Map()
|
|
@@ -75,18 +108,21 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
|
75
108
|
// 初始化即取走最小层级,因此「延迟到第 N 层」的条目在首次递归 pass 就能解锁;
|
|
76
109
|
// 层级只增不减(:5131),已打开的层级对后续 pass 保持打开。
|
|
77
110
|
const delayLevels = [...new Set(entries
|
|
78
|
-
.map(config => config.params.
|
|
111
|
+
.map(config => config.params.activation.delayUntilRecursion)
|
|
79
112
|
.filter(Boolean)
|
|
80
113
|
.map(value => value === true ? 1 : value))].sort((a, b) => a - b)
|
|
81
114
|
let delayLevel = delayLevels.shift() ?? 0
|
|
82
|
-
|
|
115
|
+
// 池是全批共享的(对齐 ST `world_info_recursive` 的全局扫描缓冲:新入选正文都进同一个池),
|
|
116
|
+
// 这里只决定「池是否产生内容、pass 是否由递归驱动」(层级池推进依赖后者,缺省时既有语义不变);
|
|
117
|
+
// 谁参与重扫按模块判,来源格式中的书级或条目级递归记录不参与运行。
|
|
118
|
+
const recursive = entries.some(config => config.contextActivationRecursive === true)
|
|
83
119
|
/** 键匹配器(按 params 对象缓存;签名含插值后的键与匹配选项)。 */
|
|
84
120
|
const matcherOf = (config, material) => {
|
|
85
121
|
const p = config.params
|
|
86
|
-
const signature = JSON.stringify([material.keys, material.secondaryKeys, p.caseSensitive, p.wholeWords, p.useRegex])
|
|
122
|
+
const signature = JSON.stringify([material.keys, material.secondaryKeys, p.caseSensitive, p.wholeWords, p.useRegex, p.wordBoundary])
|
|
87
123
|
let compiled = matchers.get(p)
|
|
88
124
|
if (compiled?.signature !== signature) {
|
|
89
|
-
compiled = { signature, matcher: createAnchorMatcher({ keys: material.keys, secondaryKeys: material.secondaryKeys, caseSensitive: p.caseSensitive === true, wholeWords: p.wholeWords === true, useRegex: p.useRegex,
|
|
125
|
+
compiled = { signature, matcher: createAnchorMatcher({ keys: material.keys, secondaryKeys: material.secondaryKeys, caseSensitive: p.caseSensitive === true, wholeWords: p.wholeWords === true, useRegex: p.useRegex, wordBoundary: p.wordBoundary }) }
|
|
90
126
|
matchers.set(p, compiled)
|
|
91
127
|
}
|
|
92
128
|
return compiled.matcher
|
|
@@ -98,122 +134,126 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
|
98
134
|
// 每 pass 的扫描材料缓存:递归文本随 pass 变化,材料必须按 pass 重新构造;
|
|
99
135
|
// 材料只被匹配与评分读取,构造本身不改变入选结果。
|
|
100
136
|
const scanCache = new Map()
|
|
101
|
-
const scanOf = (config,
|
|
137
|
+
const scanOf = (config, activation) => {
|
|
102
138
|
let material = scanCache.get(config)
|
|
103
139
|
if (material !== undefined) return material
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const
|
|
110
|
-
|
|
140
|
+
const history = activation.scan === 'history'
|
|
141
|
+
const depth = count(activation.scanDepth, history ? 2 : batch.length)
|
|
142
|
+
const parts = depth === 0 ? [] : (history ? chat : batch).slice(-depth).reverse().map(message => message.text)
|
|
143
|
+
const variables = readVariables(config)
|
|
144
|
+
const render = (text, source) => evaluateTemplate(text, { ...variables, session, sourceId: `${config.sourceModuleId ?? ''}:${config.id}:${source}`, readOnly: true }).text
|
|
145
|
+
for (const key of activation.scanVariables ?? []) {
|
|
146
|
+
const value = Object.hasOwn(variables.module, key) ? variables.module[key] : Object.hasOwn(variables.global, key) ? variables.global[key] : undefined
|
|
147
|
+
if (depth > 0 && typeof value === 'string') parts.push(render(value, `scan:${key}`))
|
|
111
148
|
}
|
|
149
|
+
// 池是全批共享的;开关没开的模块靠下面的 pass 门控不参与重扫(隔离不靠分池)。
|
|
112
150
|
if (pass > 0 && depth > 0) parts.push(...recursiveText)
|
|
113
151
|
const p = config.params
|
|
114
|
-
const keys = (
|
|
152
|
+
const keys = (p.keys ?? []).map((key, index) => render(key, `keys:${index}`)).filter(Boolean)
|
|
115
153
|
// 空宏仍是一项未命中的副键:匹配器忽略空值,但 AND_ALL/NOT_ALL 的总数不能缩水。
|
|
116
|
-
const secondaryKeys = (
|
|
154
|
+
const secondaryKeys = (p.secondaryKeys ?? []).map((key, index) => render(key, `secondaryKeys:${index}`))
|
|
117
155
|
material = { depth, keys, secondaryKeys, text: parts.length ? '\x01' + parts.join('\n\x01') : '' }
|
|
118
156
|
scanCache.set(config, material)
|
|
119
157
|
return material
|
|
120
158
|
}
|
|
121
159
|
// ST getScore 等价(world-info.js:428-473):只统计命中数,否定逻辑(NOT_ALL/NOT_ANY)
|
|
122
160
|
// 不参与加分,主键为空记 0 分。
|
|
123
|
-
const scoreOf = (config,
|
|
124
|
-
const material = scanOf(config,
|
|
161
|
+
const scoreOf = (config, activation) => {
|
|
162
|
+
const material = scanOf(config, activation)
|
|
125
163
|
const match = matcherOf(config, material).scan(material.text)
|
|
126
164
|
if (material.keys.length === 0) return 0
|
|
127
165
|
if (material.secondaryKeys.length === 0) return match.primary
|
|
128
|
-
const logic = config.params.selectiveLogic ??
|
|
129
|
-
if (logic ===
|
|
130
|
-
if (logic ===
|
|
166
|
+
const logic = config.params.selectiveLogic ?? 'AND_ANY'
|
|
167
|
+
if (logic === 'AND_ANY') return match.primary + match.secondary
|
|
168
|
+
if (logic === 'AND_ALL') return match.secondary === material.secondaryKeys.length ? match.primary + match.secondary : match.primary
|
|
131
169
|
return match.primary
|
|
132
170
|
}
|
|
133
171
|
for (const config of priority) {
|
|
134
172
|
if (selected.has(config) || failed.has(config)) continue
|
|
135
|
-
const p = config.params,
|
|
173
|
+
const p = config.params, activation = p.activation
|
|
136
174
|
const previous = state.get(config)
|
|
137
|
-
const sticky = previous?.last !== undefined && count(
|
|
175
|
+
const sticky = previous?.last !== undefined && count(activation.sticky) > 0 && chat.length < previous.last + count(activation.sticky)
|
|
138
176
|
if (sticky) { stickyEntries.add(config); note(config, 'candidate', 'sticky') }
|
|
139
|
-
if (chat.length < count(
|
|
140
|
-
if (!sticky && previous?.last !== undefined && count(
|
|
141
|
-
&& chat.length < previous.last + count(
|
|
177
|
+
if (chat.length < count(activation.delay)) { note(config, 'excluded', 'delay', { delay: count(activation.delay), messages: chat.length }); continue }
|
|
178
|
+
if (!sticky && previous?.last !== undefined && count(activation.cooldown) > 0 && chat.length > previous.last
|
|
179
|
+
&& chat.length < previous.last + count(activation.sticky) + count(activation.cooldown)) { note(config, 'excluded', 'cooldown'); continue }
|
|
142
180
|
// ST 的延迟门控(world-info.js:4860-4868):非递归 pass 一律抑制(sticky 例外,
|
|
143
181
|
// constant 也不例外);递归 pass 中层级未到同样抑制。比较用条目原始值。
|
|
144
|
-
const delayUntil =
|
|
182
|
+
const delayUntil = activation.delayUntilRecursion
|
|
145
183
|
if (!sticky && delayUntil && (pass === 0 || delayUntil > delayLevel)) {
|
|
146
184
|
note(config, 'excluded', 'delay-until-recursion', { delayUntilRecursion: delayUntil, level: delayLevel, pass })
|
|
147
185
|
continue
|
|
148
186
|
}
|
|
149
|
-
// 递归 pass
|
|
150
|
-
//
|
|
151
|
-
|
|
187
|
+
// 递归 pass 的参与资格:条目自己的 excludeRecursion(ST world-info.js:4870)与本模块开关
|
|
188
|
+
// 都要过。延迟条目由层级池驱动——层级池是全批时钟(层级只增不减),与递归正文无关,
|
|
189
|
+
// 因此不受模块递归开关门控,开关缺省时它的既有语义不变。
|
|
190
|
+
if (pass > 0 && activation.excludeRecursion === true) { note(config, 'excluded', 'recursion'); continue }
|
|
191
|
+
if (pass > 0 && config.contextActivationRecursive !== true && !delayUntil) { note(config, 'excluded', 'recursion'); continue }
|
|
152
192
|
let active = sticky || p.constant === true
|
|
153
|
-
|
|
193
|
+
const reason = sticky ? 'sticky' : p.constant === true ? 'constant' : 'key-match'
|
|
154
194
|
if (!active) {
|
|
155
|
-
const material = scanOf(config, st)
|
|
156
195
|
try {
|
|
196
|
+
const material = scanOf(config, activation)
|
|
157
197
|
const match = matcherOf(config, material).scan(material.text)
|
|
158
198
|
active = match.primary > 0
|
|
159
|
-
const logic = p.selectiveLogic ??
|
|
199
|
+
const logic = p.selectiveLogic ?? 'AND_ANY'
|
|
160
200
|
const detail = { scanDepth: material.depth, primary: match.primary, secondary: match.secondary, secondaryKeys: material.secondaryKeys.length, logic }
|
|
161
|
-
if (active &&
|
|
162
|
-
active = logic ===
|
|
163
|
-
: logic ===
|
|
201
|
+
if (active && activation.selective === true && material.secondaryKeys.length > 0) {
|
|
202
|
+
active = logic === 'NOT_ALL' ? match.secondary < material.secondaryKeys.length : logic === 'NOT_ANY' ? match.secondary === 0
|
|
203
|
+
: logic === 'AND_ALL' ? match.secondary === material.secondaryKeys.length : match.secondary > 0
|
|
164
204
|
if (!active) note(config, 'rejected', 'secondary-miss', detail)
|
|
165
205
|
} else if (!active) {
|
|
166
206
|
note(config, 'rejected', 'primary-miss', detail)
|
|
167
207
|
}
|
|
168
208
|
} catch (error) {
|
|
169
209
|
note(config, 'rejected', 'match-error', { message: String(error?.message ?? error).slice(0, 120) })
|
|
170
|
-
warn(`prompt-config-engine:
|
|
210
|
+
warn(`prompt-config-engine: 上下文激活 ${config.id} 匹配失败:${String(error?.message ?? error)}`)
|
|
171
211
|
failed.add(config)
|
|
172
212
|
continue
|
|
173
213
|
}
|
|
174
214
|
}
|
|
175
215
|
if (!active) continue
|
|
176
|
-
const probability = typeof
|
|
216
|
+
const probability = typeof activation.probability === 'number' && Number.isFinite(activation.probability) ? Math.max(0, Math.min(100, activation.probability)) : 100
|
|
177
217
|
const roll = previous?.generation === generation ? previous.roll : Math.random() * 100
|
|
178
218
|
state.set(config, { ...previous, generation, roll })
|
|
179
|
-
if (!sticky &&
|
|
219
|
+
if (!sticky && activation.useProbability !== false && (probability <= 0 || roll >= probability)) {
|
|
180
220
|
note(config, 'rejected', 'probability', { probability, roll })
|
|
181
221
|
failed.add(config)
|
|
182
222
|
continue
|
|
183
223
|
}
|
|
184
|
-
note(config, 'candidate',
|
|
224
|
+
note(config, 'candidate', reason, { probability, useProbability: activation.useProbability !== false })
|
|
185
225
|
candidates.push(config)
|
|
186
226
|
}
|
|
187
227
|
if (!candidates.length && !delayLevels.length) break
|
|
188
228
|
const groups = new Map()
|
|
189
229
|
for (const config of candidates) {
|
|
190
|
-
const names = String(config.params.
|
|
230
|
+
const names = String(config.params.activation.selectionGroup ?? '').split(',').map(name => name.trim()).filter(Boolean)
|
|
191
231
|
if (!names.length) { selected.add(config); note(config, 'selected', 'ungrouped'); continue }
|
|
192
232
|
if (names.some(name => occupied.has(name))) { note(config, 'rejected', 'group-occupied'); failed.add(config); continue }
|
|
193
233
|
for (const name of names) { const members = groups.get(name) ?? []; members.push(config); groups.set(name, members) }
|
|
194
234
|
}
|
|
195
235
|
for (const [name, members] of groups) {
|
|
196
236
|
if (occupied.has(name)) continue
|
|
197
|
-
let remaining = members.filter(config => !failed.has(config) && !String(config.params.
|
|
237
|
+
let remaining = members.filter(config => !failed.has(config) && !String(config.params.activation.selectionGroup).split(',').some(group => occupied.has(group.trim())))
|
|
198
238
|
if (!remaining.length) continue
|
|
199
239
|
const sticky = remaining.filter(config => stickyEntries.has(config))
|
|
200
240
|
// 组内评分(ST filterGroupsByScoring 5292-5328):无全局开关时,只要组内存在显式
|
|
201
241
|
// 开启 useGroupScoring 的条目就整组参与评分;组内有 sticky 时整组跳过(5300-5305)。
|
|
202
242
|
// 只有开启评分的条目会被淘汰(严格小于最高分);未开启者不被淘汰,但其分数计入最高分。
|
|
203
243
|
const scoreOut = new Set()
|
|
204
|
-
if (!sticky.length && remaining.some(config => config.params.
|
|
244
|
+
if (!sticky.length && remaining.some(config => config.params.activation.useGroupScoring === true)) {
|
|
205
245
|
let scores
|
|
206
246
|
try {
|
|
207
|
-
scores = remaining.map(config => scoreOf(config, config.params.
|
|
247
|
+
scores = remaining.map(config => scoreOf(config, config.params.activation))
|
|
208
248
|
} catch (error) {
|
|
209
249
|
scores = undefined
|
|
210
|
-
warn(`prompt-config-engine:
|
|
250
|
+
warn(`prompt-config-engine: 上下文激活分组评分失败:${String(error?.message ?? error)}`)
|
|
211
251
|
}
|
|
212
252
|
if (scores !== undefined) {
|
|
213
253
|
const maxScore = Math.max(...scores)
|
|
214
254
|
const survivors = []
|
|
215
255
|
for (const [index, config] of remaining.entries()) {
|
|
216
|
-
if (config.params.
|
|
256
|
+
if (config.params.activation.useGroupScoring !== true || scores[index] >= maxScore) { survivors.push(config); continue }
|
|
217
257
|
note(config, 'rejected', 'group-score-lost', { group: name, score: scores[index], maxScore })
|
|
218
258
|
scoreOut.add(config)
|
|
219
259
|
failed.add(config)
|
|
@@ -222,17 +262,17 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
|
222
262
|
if (!remaining.length) continue
|
|
223
263
|
}
|
|
224
264
|
}
|
|
225
|
-
const overriding = sticky.length ? sticky : remaining.filter(config => config.params.
|
|
265
|
+
const overriding = sticky.length ? sticky : remaining.filter(config => config.params.activation.groupOverride === true)
|
|
226
266
|
const available = overriding.length ? overriding : remaining
|
|
227
|
-
const weight = config => typeof config.params.
|
|
267
|
+
const weight = config => typeof config.params.activation.groupWeight === 'number' ? Math.max(0, config.params.activation.groupWeight) : 100
|
|
228
268
|
let choice = available[0]
|
|
229
269
|
if (!overriding.length) {
|
|
230
270
|
let roll = (state.get(choice).roll / 100) * available.reduce((sum, config) => sum + weight(config), 0)
|
|
231
271
|
for (const config of available) { roll -= weight(config); if (roll < 0) { choice = config; break } }
|
|
232
272
|
}
|
|
233
273
|
selected.add(choice)
|
|
234
|
-
note(choice, 'selected', 'group-winner', { group: String(choice.params.
|
|
235
|
-
for (const group of String(choice.params.
|
|
274
|
+
note(choice, 'selected', 'group-winner', { group: String(choice.params.activation.selectionGroup ?? '') })
|
|
275
|
+
for (const group of String(choice.params.activation.selectionGroup).split(',').map(value => value.trim()).filter(Boolean)) occupied.add(group)
|
|
236
276
|
for (const config of members) {
|
|
237
277
|
// 已按评分淘汰的成员只记 group-score-lost:它没有参与最后的组内竞争,
|
|
238
278
|
// 再记一条 group-lost 会让诊断给出两个互相矛盾的原因。
|
|
@@ -246,11 +286,11 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
|
|
|
246
286
|
for (const config of added) {
|
|
247
287
|
const previous = state.get(config)
|
|
248
288
|
// sticky 期间的重发不延长有效期;新命中才开始下一段窗口。
|
|
249
|
-
if (previous.last === undefined || chat.length >= previous.last + count(config.params.
|
|
289
|
+
if (previous.last === undefined || chat.length >= previous.last + count(config.params.activation.sticky)) updates.set(config, { ...previous, last: chat.length })
|
|
250
290
|
}
|
|
251
291
|
// 递归驱动优先于延迟层级(ST world-info.js:5097-5133):有新正文可递归时层级保持不变;
|
|
252
292
|
// 没有新正文而层级池仍有剩余时打开下一层继续扫描;两者都没有才结束。
|
|
253
|
-
const recursiveAdds =
|
|
293
|
+
const recursiveAdds = recursive ? added.filter(config => config.params.activation.preventRecursion !== true) : []
|
|
254
294
|
if (recursiveAdds.length) recursiveText.push(...recursiveAdds.map(config => config.texts.join('\n')))
|
|
255
295
|
else if (delayLevels.length) delayLevel = delayLevels.shift()
|
|
256
296
|
else break
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { historyEvents, viewRestricted } from './history.mjs'
|
|
2
|
+
import { pluginKind, pluginOwner } from './source-kind.mjs'
|
|
3
|
+
|
|
4
|
+
export function eventMessage(event) {
|
|
5
|
+
const data = event?.data
|
|
6
|
+
if (data === null || typeof data !== 'object') return undefined
|
|
7
|
+
const message = data.message !== null && typeof data.message === 'object' ? data.message : data
|
|
8
|
+
return message?.source !== null && typeof message?.source === 'object' ? message : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function moduleIdOf(value, field) {
|
|
12
|
+
const id = value?.[field]
|
|
13
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const moduleOf = source => moduleIdOf(source, 'moduleId')
|
|
17
|
+
export const identityOf = config => config.mergeMode === 'merged' ? `merged:${config.position}` : config.identity.value
|
|
18
|
+
|
|
19
|
+
export function pluginIdentityOf(source) {
|
|
20
|
+
if (typeof source?.plugin === 'string' && source.plugin.length > 0) return source.plugin
|
|
21
|
+
return pluginOwner(source?.kind)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function identityStampOf(config) {
|
|
25
|
+
const plugin = identityOf(config)
|
|
26
|
+
const moduleId = moduleIdOf(config, 'sourceModuleId')
|
|
27
|
+
return {
|
|
28
|
+
kind: typeof config.sourceKind === 'string' && config.sourceKind.length > 0 ? config.sourceKind : pluginKind(plugin),
|
|
29
|
+
plugin,
|
|
30
|
+
...(moduleId === undefined ? {} : { moduleId }),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function identityHit(source, config, value) {
|
|
35
|
+
if (source?.kind !== config.sourceKind && pluginIdentityOf(source) !== value) return false
|
|
36
|
+
const moduleId = moduleOf(source)
|
|
37
|
+
return moduleId === undefined || moduleId === moduleIdOf(config, 'sourceModuleId')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 投递关联与去重读取相同的可见内容,投影删除的消息不能再充当事实。 */
|
|
41
|
+
export function visibleMessages(session) {
|
|
42
|
+
const events = historyEvents(session)
|
|
43
|
+
const visible = viewRestricted(session, events) ? session.surface.nodes.map(seq => events[seq]) : events
|
|
44
|
+
const seen = new Set()
|
|
45
|
+
const out = []
|
|
46
|
+
for (const event of visible) {
|
|
47
|
+
const seq = Number.isSafeInteger(event.seq) ? event.seq : events.indexOf(event)
|
|
48
|
+
if (seq < 0 || seen.has(seq)) continue
|
|
49
|
+
seen.add(seq)
|
|
50
|
+
const message = typeof session?.surface?.deriveEventMessage === 'function'
|
|
51
|
+
? session.surface.deriveEventMessage(event) : eventMessage(event)
|
|
52
|
+
if (message != null) out.push({ event, seq, message })
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function injectedMessage(config, session) {
|
|
58
|
+
const value = identityOf(config)
|
|
59
|
+
return visibleMessages(session).find(({ message, seq }) => identityHit(message.source, config, value)
|
|
60
|
+
&& (!config.linkedDelivery || message.source.sessionId === session.id && seq >= (session.inheritedEventCount ?? 0)))?.message
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function actionStamp(config, session) {
|
|
64
|
+
return config.ruleId === undefined || config.ruleActionId === undefined || session?.id === undefined ? {} : {
|
|
65
|
+
ruleId: config.ruleId, actionId: config.ruleActionId, sessionId: session?.id,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 官方同 order 按 name 排序,注册和投递查询共用这一份名字。 */
|
|
70
|
+
export function textRegistrationName(config, field) {
|
|
71
|
+
const explicit = config.params?.[field]
|
|
72
|
+
if (typeof explicit === 'string' && explicit.length > 0) return explicit
|
|
73
|
+
if (config.sourceModuleId === undefined) return config.id
|
|
74
|
+
return `prompt-tool:${String(config.sequence).padStart(16, '0')}:${config.sourceModuleId}:${config.id}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** after 需要动作的严格证据;去重的宽松身份命中不能代替这份关联。 */
|
|
78
|
+
export function findActionDelivery(session, config, messages = visibleMessages(session)) {
|
|
79
|
+
if (config === undefined || session?.id === undefined) return undefined
|
|
80
|
+
let found
|
|
81
|
+
for (const entry of messages) {
|
|
82
|
+
if (entry.seq < (session?.inheritedEventCount ?? 0)) continue
|
|
83
|
+
const source = entry.message.source
|
|
84
|
+
const matches = config.layer === 'runtime-context'
|
|
85
|
+
? source?.kind === 'runtime-context' && source.form === 'snapshot'
|
|
86
|
+
&& source.sections?.some(section => section.name === textRegistrationName(config, 'contextName') && typeof section.text === 'string' && section.text.length > 0)
|
|
87
|
+
: (source?.moduleId ?? '') === (config.sourceModuleId ?? '')
|
|
88
|
+
&& source?.ruleId === config.ruleId && source?.actionId === config.ruleActionId
|
|
89
|
+
&& source?.sessionId === session?.id
|
|
90
|
+
if (matches && (found === undefined || entry.seq > found.seq)) found = entry
|
|
91
|
+
}
|
|
92
|
+
return found
|
|
93
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** `discovery.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
|
|
2
|
+
|
|
3
|
+
export const DISCOVERY_ADAPTER_ROWS: Readonly<Record<string, string>>
|
|
4
|
+
export const UNLOCKABLE_DISCOVERY_KINDS: ReadonlySet<string>
|
|
5
|
+
|
|
6
|
+
export interface DiscoveryUnlock {
|
|
7
|
+
tool: string
|
|
8
|
+
key: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DiscoveryAdapterMetadata {
|
|
12
|
+
kind: string
|
|
13
|
+
tool: string
|
|
14
|
+
unlock?: DiscoveryUnlock
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoveryRegistry {
|
|
18
|
+
register(agent: unknown, kind: string, adapter: DiscoveryAdapterMetadata): () => void
|
|
19
|
+
get(agent: unknown, kind: string): DiscoveryAdapterMetadata | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDiscoveryRegistry(): DiscoveryRegistry
|
|
23
|
+
export function assertDiscoveryRows(rules: readonly unknown[], moduleIds: readonly string[] | undefined, moduleId: string): void
|
|
24
|
+
export function queryTokens(query: unknown): string[]
|
|
25
|
+
export function scoreRows(items: readonly unknown[], tokens: readonly string[], options?: {
|
|
26
|
+
nameOf?: (item: unknown) => unknown
|
|
27
|
+
haystackOf?: (item: unknown) => unknown
|
|
28
|
+
}): Array<{ item: unknown; name: string; score: number; exact: boolean }>
|
|
29
|
+
export function rankRows<T extends { exact: boolean; score: number; name: string }>(rows: readonly T[], admit?: (row: T) => boolean): T[]
|
|
30
|
+
export function renderRows(rows: readonly unknown[], options?: {
|
|
31
|
+
query?: string
|
|
32
|
+
limit?: number
|
|
33
|
+
noun?: string
|
|
34
|
+
lineOf?: (row: unknown) => string
|
|
35
|
+
emptyText?: string
|
|
36
|
+
footer?: string
|
|
37
|
+
warnings?: readonly string[]
|
|
38
|
+
}): string[]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* discovery —— 发现引擎:搜索、排序、截断渲染与「适配器注册表」的唯一实现。
|
|
3
|
+
*
|
|
4
|
+
* 两个数据源(工具目录 / 技能目录)共用:
|
|
5
|
+
* - 分词与候选打分(queryTokens / scoreRows);
|
|
6
|
+
* - 准入与排序(rankRows:默认 any-token,两个发现面同一口径;调用方可给更严的 admit);
|
|
7
|
+
* - 截断、空结果与页脚(renderRows)。
|
|
8
|
+
*
|
|
9
|
+
* 引擎之间通过 **cordis 服务** 互相调用,而不是直接 import 实现:
|
|
10
|
+
* - 宿主 root provide `promptToolDiscovery`(createDiscoveryRegistry);
|
|
11
|
+
* - adapter 行的 apply 在拿到 { agent } 时注册自己的元数据;
|
|
12
|
+
* - 消费方(tool-visibility)在首次装配时用 `ctx.get(DISCOVERY_SERVICE)` 按 agent 解析。
|
|
13
|
+
* 服务读取用 `ctx.get`(不需要 inject;cordis reflect.ts 的 get 明确「without the inject
|
|
14
|
+
* requirement」),属性读才需要 inject 且沿祖先链——这里刻意用 get。
|
|
15
|
+
*
|
|
16
|
+
* 注册表只存**元数据**(kind / tool / unlock / adapter 身份),工具仍由 adapter 行自己注册:
|
|
17
|
+
* 行的语义不变(挂行 = 有该工具),消费方只消费元数据(解锁名单从哪个参数回收、该用哪个工具名)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 每个 agent 的 adapter 只保留一份:同一个 adapter 重复注册是幂等的。 */
|
|
21
|
+
export function createDiscoveryRegistry() {
|
|
22
|
+
const byAgent = new WeakMap()
|
|
23
|
+
const assertAdapter = (kind, adapter) => {
|
|
24
|
+
if (typeof kind !== 'string' || kind.length === 0) throw new TypeError('discovery adapter kind must be a non-empty string')
|
|
25
|
+
if (adapter === null || typeof adapter !== 'object') throw new TypeError(`discovery adapter "${kind}" must be an object`)
|
|
26
|
+
if (typeof adapter.tool !== 'string' || adapter.tool.length === 0) {
|
|
27
|
+
throw new TypeError(`discovery adapter "${kind}" needs a tool name`)
|
|
28
|
+
}
|
|
29
|
+
if (adapter.unlock !== undefined) {
|
|
30
|
+
for (const field of ['tool', 'key']) {
|
|
31
|
+
if (typeof adapter.unlock[field] !== 'string' || adapter.unlock[field].length === 0) {
|
|
32
|
+
throw new TypeError(`discovery adapter "${kind}" unlock.${field} must be a non-empty string`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
/**
|
|
39
|
+
* 注册一份 adapter 元数据;同一 agent 同一 kind 只允许一个实现。
|
|
40
|
+
* 同一 adapter 身份重复注册(同一行在重绑/多次装配中再次 apply)是幂等的。
|
|
41
|
+
*/
|
|
42
|
+
register(agent, kind, adapter) {
|
|
43
|
+
if (agent === undefined || agent === null) return () => {}
|
|
44
|
+
assertAdapter(kind, adapter)
|
|
45
|
+
let entries = byAgent.get(agent)
|
|
46
|
+
if (entries === undefined) {
|
|
47
|
+
entries = new Map()
|
|
48
|
+
byAgent.set(agent, entries)
|
|
49
|
+
}
|
|
50
|
+
const existing = entries.get(kind)
|
|
51
|
+
if (existing !== undefined) {
|
|
52
|
+
if (existing === adapter) return () => {}
|
|
53
|
+
throw new Error(`discovery kind "${kind}" is already registered for this agent`)
|
|
54
|
+
}
|
|
55
|
+
entries.set(kind, adapter)
|
|
56
|
+
return () => { if (entries.get(kind) === adapter) entries.delete(kind) }
|
|
57
|
+
},
|
|
58
|
+
/** 解析某 agent 的 adapter 元数据;没有注册过时返回 undefined(发现是可选能力)。 */
|
|
59
|
+
get(agent, kind) {
|
|
60
|
+
if (agent === undefined || agent === null) return undefined
|
|
61
|
+
return byAgent.get(agent)?.get(kind)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** kind → 提供该 adapter 的组合行 id(准备期静态校验:声明了 discovery 就必须列对应行)。 */
|
|
67
|
+
export const DISCOVERY_ADAPTER_ROWS = Object.freeze({
|
|
68
|
+
tools: 'tool-search',
|
|
69
|
+
skills: 'skill-search',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/** 可按名解锁的 kind:技能面只有搜索/加载,没有「解锁名单」语义。 */
|
|
73
|
+
export const UNLOCKABLE_DISCOVERY_KINDS = new Set(['tools'])
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 准备期校验(纯函数,供 agent-assembly 调用):规则里出现的 discovery.kind 必须有对应行。
|
|
77
|
+
* 失败信息点明缺哪一行,避免「装配成功但发现能力静默缺失」。
|
|
78
|
+
*/
|
|
79
|
+
export function assertDiscoveryRows(rules, moduleIds, moduleId) {
|
|
80
|
+
const ids = new Set(Array.isArray(moduleIds) ? moduleIds : [])
|
|
81
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
82
|
+
// 未启用的规则不装配,也就不需要适配器行;启用后缺行会在下一次准备期失败。
|
|
83
|
+
if (rule?.enabled === false) continue
|
|
84
|
+
for (const action of Array.isArray(rule?.actions) ? rule.actions : []) {
|
|
85
|
+
const discovery = action?.discovery
|
|
86
|
+
if (discovery === undefined) continue
|
|
87
|
+
const kind = discovery.kind
|
|
88
|
+
const row = DISCOVERY_ADAPTER_ROWS[kind]
|
|
89
|
+
if (row === undefined) {
|
|
90
|
+
throw new TypeError(`模块 ${moduleId}:未知 discovery kind ${JSON.stringify(kind)} — 可用值:${Object.keys(DISCOVERY_ADAPTER_ROWS).join(', ')}`)
|
|
91
|
+
}
|
|
92
|
+
if (!ids.has(row)) {
|
|
93
|
+
throw new TypeError(`模块 ${moduleId} 声明 discovery kind "${kind}",但 modules 未包含 "${row}" 行 — 发现适配器必须作为引擎行装配`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 查询切成小写 token(Unicode 字母数字与下划线连字符;两个数据源同一口径)。 */
|
|
100
|
+
export function queryTokens(query) {
|
|
101
|
+
return (typeof query === 'string' ? query : '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 候选打分:每个 token 在 haystack 里出现一次记 1 分;空查询返回空数组
|
|
106
|
+
* (调用方据此区分「没给查询」与「没搜到」)。
|
|
107
|
+
*/
|
|
108
|
+
export function scoreRows(items, tokens, { nameOf, haystackOf } = {}) {
|
|
109
|
+
if (!Array.isArray(items) || tokens.length === 0) return []
|
|
110
|
+
const name = nameOf ?? ((item) => item?.name)
|
|
111
|
+
const haystack = haystackOf ?? ((item) => `${item?.name ?? ''} ${item?.description ?? ''}`)
|
|
112
|
+
return items
|
|
113
|
+
.map((item) => {
|
|
114
|
+
const itemName = String(name(item) ?? '')
|
|
115
|
+
const text = String(haystack(item) ?? '').toLowerCase()
|
|
116
|
+
let score = 0
|
|
117
|
+
for (const token of tokens) if (text.includes(token)) score += 1
|
|
118
|
+
return { item, name: itemName, score, exact: tokens.includes(itemName.toLowerCase()) }
|
|
119
|
+
})
|
|
120
|
+
.filter((row) => row.name.length > 0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 准入与排序:admit 决定哪些行算命中(默认 any-token,工具面与技能面同口径);
|
|
125
|
+
* 精确名优先,其次命中数,同分按名字字典序。
|
|
126
|
+
*/
|
|
127
|
+
export function rankRows(rows, admit = (row) => row.exact || row.score >= 1) {
|
|
128
|
+
return rows
|
|
129
|
+
.filter(admit)
|
|
130
|
+
.sort((a, b) => (b.exact - a.exact) || (b.score - a.score) || a.name.localeCompare(b.name))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 渲染命中:标题、逐行、截断提示、页脚;空结果只回 emptyText。
|
|
135
|
+
* lineOf 负责单个条目的格式(描述截断长度等由各 adapter 决定)。
|
|
136
|
+
*/
|
|
137
|
+
export function renderRows(rows, { query, limit, noun, lineOf, emptyText, footer, warnings = [] } = {}) {
|
|
138
|
+
const matches = rows.slice(0, limit)
|
|
139
|
+
if (matches.length === 0) return [emptyText]
|
|
140
|
+
const lines = [`Matching ${noun} (${matches.length}${rows.length > limit ? ` of ${rows.length}` : ''}):`]
|
|
141
|
+
for (const row of matches) lines.push(lineOf(row))
|
|
142
|
+
if (rows.length > limit) lines.push(`(truncated at ${limit} — add tokens to narrow the query)`)
|
|
143
|
+
for (const warning of warnings) lines.push(warning)
|
|
144
|
+
if (footer !== undefined && footer.length > 0) lines.push(footer)
|
|
145
|
+
return lines
|
|
146
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env 事实的安全边界:env-facts 只能读白名单里的环境变量,不能把任意 `process.env` 搬进提示词。
|
|
3
|
+
* 白名单与凭据形态判据是导出常量,供填充器与测试共用同一份事实。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 疑似凭据的名字一律不放行。 */
|
|
7
|
+
export const SENSITIVE_ENV_RE = /KEY|PASSWORD|SECRET|TOKEN/i
|
|
8
|
+
|
|
9
|
+
/** env-facts 可读取的键:run-code-env 默认白名单 + 路径类事实(CWD/WORKSPACE)。 */
|
|
10
|
+
export const SAFE_ENV_KEYS = new Set([
|
|
11
|
+
'PATH', 'PATHEXT', 'HOME', 'USERPROFILE', 'USERNAME', 'COMPUTERNAME', 'OS', 'TEMP', 'TMP',
|
|
12
|
+
'SystemRoot', 'ProgramFiles', 'ProgramFiles(x86)', 'LOCALAPPDATA', 'APPDATA',
|
|
13
|
+
'DSH_HOME', 'DSH_WORKSPACE', 'DSH_SESSION_ID', 'CWD', 'WORKSPACE',
|
|
14
|
+
])
|