dsh-plugin-prompt-tool 3.0.2 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/engine/actions/index.mjs +5 -3
- package/engine/actions/tool-visibility.mjs +4 -1
- package/engine/compositions/source/local/context-provenance.yml +9 -0
- package/engine/compositions/source/local/generation-recovery.yml +10 -0
- package/engine/compositions/source/local/retrieval.yml +7 -0
- package/engine/compositions/source/local/soul-ledger.yml +3 -0
- package/engine/compositions/source/local/soul-maintenance.yml +9 -0
- package/engine/conditions/examples.mjs +1 -0
- package/engine/conditions/index.mjs +7 -0
- package/engine/conditions/manifest.mjs +28 -0
- package/engine/context-idle-compaction.mjs +58 -0
- package/engine/context-provenance.d.mts +7 -0
- package/engine/context-provenance.mjs +156 -0
- package/engine/context-pruner.mjs +64 -0
- package/engine/context-routing.mjs +45 -0
- package/engine/context-summary.mjs +80 -0
- package/engine/fillers.mjs +11 -2
- package/engine/generation-recovery-coordinator.mjs +40 -0
- package/engine/generation-recovery-maintenance.mjs +18 -0
- package/engine/generation-recovery.mjs +153 -0
- package/engine/host-package.mjs +13 -0
- package/engine/layers.d.mts +4 -0
- package/engine/layers.mjs +57 -20
- package/engine/present-as.mjs +2 -2
- package/engine/retrieval-reference.d.mts +5 -0
- package/engine/retrieval-reference.mjs +26 -0
- package/engine/retrieval.mjs +17 -0
- package/engine/rule-engine.mjs +1 -0
- package/engine/rule-runtime.d.mts +8 -1
- package/engine/rule-runtime.mjs +18 -10
- package/engine/rule-spec.mjs +4 -2
- package/engine/schema.mjs +1 -1
- package/engine/service-names.d.mts +5 -0
- package/engine/service-names.mjs +5 -0
- package/engine/soul-ledger.mjs +8 -0
- package/engine/soul-maintenance.mjs +20 -0
- package/lib/client.js +12 -2
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +1868 -208
- package/modules/soul/assets/ledger.md +6 -2
- package/modules/soul/assets/manifest.yml +2 -0
- package/modules/soul/assets/templates/agents-template.md +3 -0
- package/modules/soul/assets/templates/self-template.md +19 -0
- package/modules/soul/assets/tools.md +10 -0
- package/modules/soul/module.yml +89 -15
- package/modules/tool-surface/module.yml +28 -6
- package/package.json +1 -1
- package/skills/dsh-module/reference.md +1 -1
- package/templates/80-tool-surface.yml +25 -4
package/engine/fillers.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fillers — 引擎内置的动态填充器(strategy=placeholder 的数据源)。
|
|
3
|
-
*
|
|
3
|
+
* 按本次求值读取宿主能力;失败由填充器或调用方诊断并跳过该配置。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { getService, parseToolNames } from './shared.mjs'
|
|
@@ -8,7 +8,7 @@ import { toolScopeSnapshot } from './tool-scope.mjs'
|
|
|
8
8
|
import { machinePaths } from './machine-paths.mjs'
|
|
9
9
|
import { SAFE_ENV_KEYS } from './env-safety.mjs'
|
|
10
10
|
import { createInstructionHintResolver } from './instruction-hint.mjs'
|
|
11
|
-
import { DISCOVERY_SERVICE } from './service-names.mjs'
|
|
11
|
+
import { DISCOVERY_SERVICE, SOUL_SELF_SERVICE } from './service-names.mjs'
|
|
12
12
|
|
|
13
13
|
const name = 'prompt-config-engine'
|
|
14
14
|
|
|
@@ -187,12 +187,21 @@ function createSkillCatalogResolver(config) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function createSoulSelfResolver(config) {
|
|
191
|
+
return ({ ctx, agent, signal }) => {
|
|
192
|
+
const service = getService(ctx, SOUL_SELF_SERVICE)
|
|
193
|
+
if (typeof service?.render !== 'function') throw new Error('SELF dependency missing')
|
|
194
|
+
return { text: service.render(config.sourceModuleId, agent, signal) }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
190
198
|
/** 动态填充器:strategy=placeholder 时按 fill 键在注入点填充内容。 */
|
|
191
199
|
export const FILLERS = {
|
|
192
200
|
'instruction-hint': (config) => createInstructionHintResolver(config),
|
|
193
201
|
'env-facts': (config) => createEnvFactsResolver(config),
|
|
194
202
|
'skill-catalog': (config) => createSkillCatalogResolver(config),
|
|
195
203
|
'tool-catalog': (config) => createToolCatalogResolver(config),
|
|
204
|
+
'soul-self': (config) => createSoulSelfResolver(config),
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
export function createPlaceholderResolver(config) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { historyEvents } from './history.mjs'
|
|
2
|
+
|
|
3
|
+
const visible = content => Array.isArray(content) && content.some(block => block.type !== 'reasoning' && (block.type !== 'text' || block.text?.trim()))
|
|
4
|
+
|
|
5
|
+
/** 候选只在 durable turn/end 后发布;流与结算共用官方 turn/step。 */
|
|
6
|
+
export function recoveryCoordinator(ctx, agent, config, options, isActive) {
|
|
7
|
+
let frame
|
|
8
|
+
let emitted
|
|
9
|
+
const offRequest = ctx.on('agent/request', (payload, next) => {
|
|
10
|
+
if (payload.agent !== agent) return next()
|
|
11
|
+
frame = { turn: payload.turn, step: payload.step, signal: payload.signal, visible: false }
|
|
12
|
+
return next()
|
|
13
|
+
})
|
|
14
|
+
const offEvent = ctx.on('session/event', (session, event) => {
|
|
15
|
+
if (!isActive() || session.id !== agent.session.id || !frame) return
|
|
16
|
+
const data = event.data ?? {}
|
|
17
|
+
if (event.type === 'assistant/message' && data.turn === frame.turn) {
|
|
18
|
+
if (data.step !== frame.step) return
|
|
19
|
+
frame.visible = visible(data.message?.content)
|
|
20
|
+
if (data.interrupted) frame.interrupted = true
|
|
21
|
+
}
|
|
22
|
+
if (event.type !== 'turn/end' || data.turn !== frame.turn || frame.signal?.aborted || frame.interrupted) return
|
|
23
|
+
if (data.reason?.kind !== 'completed' && data.reason?.kind !== 'no-visible-output') return
|
|
24
|
+
const execution = `${frame.turn}:${frame.step}`
|
|
25
|
+
if (execution === emitted || (!frame.reason && (!config.recoverEmpty || frame.visible))) return
|
|
26
|
+
emitted = execution
|
|
27
|
+
const candidate = { sessionId: session.id, execution, turn: frame.turn, step: frame.step, reason: frame.reason ?? 'empty-final-step', signal: frame.signal }
|
|
28
|
+
options.onRecoveryCandidate?.(candidate)
|
|
29
|
+
options.recover?.(candidate)
|
|
30
|
+
})
|
|
31
|
+
return {
|
|
32
|
+
mark(reason) { if (frame && !frame.signal?.aborted) frame.reason = reason },
|
|
33
|
+
dispose() { offRequest(); offEvent() },
|
|
34
|
+
// 迟到idle回调不能跨过新的用户执行边界。
|
|
35
|
+
unchanged(candidate) {
|
|
36
|
+
const latest = historyEvents(agent.session).findLast(event => event.type === 'turn/start')
|
|
37
|
+
return !latest || latest.data?.turn === candidate.turn
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 维护窗口与范围选择归上下文runtime,这里只决定是否允许扩大范围。 */
|
|
2
|
+
export async function recoveryMaintenance(runtime, signal, allowFullFallback, valid, report) {
|
|
3
|
+
if (!runtime) { report('context runtime unavailable; recovery stopped'); return null }
|
|
4
|
+
let result
|
|
5
|
+
try {
|
|
6
|
+
result = await runtime.compactRecovery(signal)
|
|
7
|
+
} catch (error) {
|
|
8
|
+
if (!valid() || error?.name === 'AbortError' || error?.code === 'ABORTED') return null
|
|
9
|
+
if (!allowFullFallback) { report(`regional maintenance failed; recovery stopped (${String(error?.message ?? error)})`); return null }
|
|
10
|
+
result = await runtime.compactNow(signal)
|
|
11
|
+
}
|
|
12
|
+
if (!valid()) return null
|
|
13
|
+
if (result === null || !Array.isArray(result?.shadowedSeqs) || result.shadowedSeqs.length === 0) {
|
|
14
|
+
report('maintenance no-op; recovery stopped')
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
return result
|
|
18
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { bool, defineConfig, enumOf, int, passthrough } from './fields.mjs'
|
|
2
|
+
import { capabilityAudience } from './agent-capability.mjs'
|
|
3
|
+
import { recoveryCoordinator } from './generation-recovery-coordinator.mjs'
|
|
4
|
+
import { pluginMessage } from './layers.mjs'
|
|
5
|
+
import { recoveryMaintenance } from './generation-recovery-maintenance.mjs'
|
|
6
|
+
|
|
7
|
+
export const name = 'generation-recovery'
|
|
8
|
+
export const inject = ['llm']
|
|
9
|
+
|
|
10
|
+
const recoveryLimit = passthrough((value, plugin, field) => {
|
|
11
|
+
if (value === undefined) return 0
|
|
12
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 1000) throw new TypeError(`${plugin}: ${field} must be an integer from 0 to 1000`)
|
|
13
|
+
return value
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const configContract = defineConfig({
|
|
17
|
+
enabled: bool({ default: false }),
|
|
18
|
+
audience: enumOf(['all', 'main', 'subagent'], { default: 'all' }),
|
|
19
|
+
textRepeatChars: int({ min: 3, default: 4096 }),
|
|
20
|
+
reasoningRepeatChars: int({ min: 3, default: 8192 }),
|
|
21
|
+
silentReasoningChars: int({ min: 0, default: 0 }),
|
|
22
|
+
recoverEmpty: bool({ default: false }),
|
|
23
|
+
mainRecoveryLimit: recoveryLimit,
|
|
24
|
+
subagentRecoveryLimit: recoveryLimit,
|
|
25
|
+
maintenanceCompression: bool({ default: false }),
|
|
26
|
+
allowFullFallback: bool({ default: false }),
|
|
27
|
+
maxCycleChars: passthrough((value, plugin, field) => {
|
|
28
|
+
if (value === undefined) return 256
|
|
29
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 1024) throw new TypeError(`${plugin}: ${field} must be an integer from 1 to 1024`)
|
|
30
|
+
return value
|
|
31
|
+
}),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// 逐 Unicode 字符比较周期,内存只随最长周期增长,不保留整段推理。
|
|
35
|
+
function repeatDetector(threshold, maxCycle) {
|
|
36
|
+
const tail = new Array(maxCycle)
|
|
37
|
+
const matches = new Float64Array(maxCycle + 1)
|
|
38
|
+
let seen = 0
|
|
39
|
+
return text => {
|
|
40
|
+
let offset = 0
|
|
41
|
+
for (const char of text) {
|
|
42
|
+
offset += char.length
|
|
43
|
+
let repeated = false
|
|
44
|
+
for (let period = 1; period <= Math.min(seen, maxCycle); period++) {
|
|
45
|
+
const needed = Math.max(threshold - period, 2 * period)
|
|
46
|
+
matches[period] = tail[(seen - period) % maxCycle] === char ? Math.min(matches[period] + 1, needed) : 0
|
|
47
|
+
if (matches[period] >= needed) repeated = true
|
|
48
|
+
}
|
|
49
|
+
tail[seen % maxCycle] = char
|
|
50
|
+
seen++
|
|
51
|
+
if (repeated) return offset
|
|
52
|
+
}
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function apply(ctx, raw, options = {}) {
|
|
58
|
+
const { agent } = options
|
|
59
|
+
const config = configContract.parse(raw, name)
|
|
60
|
+
if (!config.enabled || typeof agent?.session?.id !== 'string') return
|
|
61
|
+
if (config.audience !== 'all' && config.audience !== capabilityAudience(agent)) return
|
|
62
|
+
let active = true
|
|
63
|
+
const report = message => ctx.logger?.warn?.(`${name}: ${message}`)
|
|
64
|
+
const limit = capabilityAudience(agent) === 'subagent' ? config.subagentRecoveryLimit : config.mainRecoveryLimit
|
|
65
|
+
const coordinator = recoveryCoordinator(ctx, agent, config, { ...options, recover(candidate) {
|
|
66
|
+
if (!limit) return
|
|
67
|
+
if (!options.recoveryState) { report('persistent recovery state unavailable; automatic recovery disabled'); return }
|
|
68
|
+
try {
|
|
69
|
+
const claimed = options.recoveryState.claim(agent.session.id, candidate.execution, limit)
|
|
70
|
+
if (!claimed.claimed) { report(`recovery unavailable (${claimed.used}/${limit})`); return }
|
|
71
|
+
void (async () => {
|
|
72
|
+
try {
|
|
73
|
+
await agent.whenIdle()
|
|
74
|
+
const valid = () => active && !candidate.signal?.aborted && coordinator.unchanged(candidate)
|
|
75
|
+
if (!valid()) return
|
|
76
|
+
let maintenance
|
|
77
|
+
if (config.maintenanceCompression) {
|
|
78
|
+
maintenance = await recoveryMaintenance(options.contextRuntimeOf?.(agent), candidate.signal, config.allowFullFallback, valid, report)
|
|
79
|
+
if (!maintenance) return
|
|
80
|
+
}
|
|
81
|
+
if (!valid()) return
|
|
82
|
+
const text = '上一轮输出因异常重复或仅有推理而未完成。请停止重复,检查已完成的工作,并给出有效结果或明确说明阻塞原因。'
|
|
83
|
+
+ (maintenance ? `\n维护压缩已完成,实际来源事件:${maintenance.shadowedSeqs.join(',')}。` : '')
|
|
84
|
+
agent.followup(pluginMessage('generation-recovery', text, '恢复异常生成'))
|
|
85
|
+
} catch (error) { report(String(error?.message ?? error)) }
|
|
86
|
+
})()
|
|
87
|
+
} catch (error) { report(String(error?.message ?? error)) }
|
|
88
|
+
} }, () => active)
|
|
89
|
+
const stop = ctx.effect(() => () => { active = false })
|
|
90
|
+
const off = ctx.on('llm/stream', (options, next) => {
|
|
91
|
+
if (!active || options.signal?.aborted || options.purpose !== undefined || options.sessionId !== agent.session.id) return next()
|
|
92
|
+
return (async function* () {
|
|
93
|
+
const detectors = {
|
|
94
|
+
text: repeatDetector(config.textRepeatChars, config.maxCycleChars),
|
|
95
|
+
reasoning: repeatDetector(config.reasoningRepeatChars, config.maxCycleChars),
|
|
96
|
+
}
|
|
97
|
+
const open = new Map()
|
|
98
|
+
const closed = new Set()
|
|
99
|
+
const finalized = new Set()
|
|
100
|
+
let hasToolCall = false
|
|
101
|
+
let visibleText = false
|
|
102
|
+
let reasoningChars = 0
|
|
103
|
+
for await (const chunk of next()) {
|
|
104
|
+
if (finalized.has(chunk.index)) continue
|
|
105
|
+
if (!active || options.signal?.aborted) { yield chunk; continue }
|
|
106
|
+
if (chunk.type === 'tool-call-delta' || chunk.blockType === 'tool-call' || chunk.block?.type === 'tool-call') hasToolCall = true
|
|
107
|
+
if (chunk.type === 'block-start') open.set(chunk.index, { type: chunk.blockType, text: '' })
|
|
108
|
+
if (chunk.type === 'block-end') { open.delete(chunk.index); closed.add(chunk.index) }
|
|
109
|
+
const kind = chunk.type === 'text-delta' ? 'text' : chunk.type === 'reasoning-delta' ? 'reasoning' : undefined
|
|
110
|
+
if (kind === undefined || closed.has(chunk.index)) { yield chunk; continue }
|
|
111
|
+
if (!open.has(chunk.index)) open.set(chunk.index, { type: kind, text: '' })
|
|
112
|
+
// 已闭合工具也要保留原始finish语义,不把工具回合伪装成普通stop。
|
|
113
|
+
const safe = !hasToolCall && [...open.values()].every(block => block.type === 'text' || block.type === 'reasoning')
|
|
114
|
+
if (kind === 'text' && chunk.text.trim()) visibleText = true
|
|
115
|
+
let cutoff = safe ? detectors[kind](chunk.text) : undefined
|
|
116
|
+
let cause = 'repeated-' + kind
|
|
117
|
+
if (safe && kind === 'reasoning' && !visibleText && config.silentReasoningChars > 0) {
|
|
118
|
+
const chars = [...chunk.text]
|
|
119
|
+
const remaining = config.silentReasoningChars - reasoningChars
|
|
120
|
+
reasoningChars += chars.length
|
|
121
|
+
if (remaining > 0 && remaining <= chars.length) {
|
|
122
|
+
const silentCutoff = chars.slice(0, remaining).join('').length
|
|
123
|
+
if (cutoff === undefined || silentCutoff < cutoff) { cutoff = silentCutoff; cause = 'silent-reasoning' }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const text = cutoff === undefined ? chunk.text : chunk.text.slice(0, cutoff)
|
|
127
|
+
const block = open.get(chunk.index)
|
|
128
|
+
if (block !== undefined) block.text += text
|
|
129
|
+
yield cutoff === undefined ? chunk : { ...chunk, text }
|
|
130
|
+
if (cutoff === undefined) continue
|
|
131
|
+
if (!active || options.signal?.aborted) {
|
|
132
|
+
const remainder = chunk.text.slice(cutoff)
|
|
133
|
+
if (remainder) yield { ...chunk, text: remainder }
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
for (const [index, value] of open) {
|
|
137
|
+
if (!active || options.signal?.aborted) break
|
|
138
|
+
finalized.add(index)
|
|
139
|
+
yield { type: 'block-end', index, block: value }
|
|
140
|
+
}
|
|
141
|
+
if (!active || options.signal?.aborted) {
|
|
142
|
+
const remainder = chunk.text.slice(cutoff)
|
|
143
|
+
if (!finalized.has(chunk.index) && remainder) yield { ...chunk, text: remainder }
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
coordinator.mark(cause)
|
|
147
|
+
yield { type: 'finish', reason: { kind: 'stop' } }
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
})()
|
|
151
|
+
})
|
|
152
|
+
return () => { stop(); off(); coordinator.dispose() }
|
|
153
|
+
}
|
package/engine/host-package.mjs
CHANGED
|
@@ -49,3 +49,16 @@ export function resolveHostPackage(id, entry, self) {
|
|
|
49
49
|
export async function importHostPackage(id, entry, self) {
|
|
50
50
|
return import(pathToFileURL(resolveHostPackage(id, entry, self)).href)
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** 允许从宿主解析的官方能力包;版本随宿主(alpha/rc 均可),不在插件内做精确匹配。 */
|
|
54
|
+
export const HOST_CAPABILITY_PACKAGES = Object.freeze([
|
|
55
|
+
'@deepseek-ai/dsh-compaction-basic',
|
|
56
|
+
'@deepseek-ai/dsh-compaction-tool-result-pruner',
|
|
57
|
+
'@deepseek-ai/dsh-compaction',
|
|
58
|
+
'@deepseek-ai/dsh-llm',
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
export async function importHostCapability(id) {
|
|
62
|
+
if (!HOST_CAPABILITY_PACKAGES.includes(id)) throw new Error(`host capability not declared: ${id}`)
|
|
63
|
+
return importHostPackage(id)
|
|
64
|
+
}
|
package/engine/layers.mjs
CHANGED
|
@@ -183,7 +183,7 @@ function textLayerGroups(configs) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
|
|
186
|
-
function wireSystemSections(ctx, configs, registry, warnOnce, keep, ordered) {
|
|
186
|
+
function wireSystemSections(ctx, configs, registry, warnOnce, keep, ordered, sourceError) {
|
|
187
187
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
188
188
|
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
189
189
|
if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — system-section configs skipped`)
|
|
@@ -218,6 +218,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce, keep, ordered) {
|
|
|
218
218
|
keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
|
|
219
219
|
}
|
|
220
220
|
} catch (error) {
|
|
221
|
+
if (sourceError !== undefined) throw sourceError(error, base.sourceModuleId)
|
|
221
222
|
warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
222
223
|
}
|
|
223
224
|
}
|
|
@@ -240,7 +241,7 @@ async function resolvedContextText(ctx, config, context, registry, warnOnce) {
|
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
/** 条件化文本保留官方排序占位;仅在本次真实 assembly 填充,不缓存 context 身份。 */
|
|
243
|
-
function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, ordered) {
|
|
244
|
+
function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, ordered, sourceError) {
|
|
244
245
|
if (!configs.length) return []
|
|
245
246
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
246
247
|
if (systemPrompt === undefined) { warnOnce(`${name}: systemPrompt unavailable`); return [] }
|
|
@@ -252,8 +253,26 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, o
|
|
|
252
253
|
const section = layer === 'system-section'
|
|
253
254
|
const base = group[0]
|
|
254
255
|
const entryName = textRegistrationName(base, section ? 'sectionName' : 'contextName')
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
// runtime-context 在官方 refreshContext 里按注册 provider 重算:provider 必须返回当前文本,
|
|
257
|
+
// 否则 assemble 钩子的填充会在请求前被 marker 覆盖。system-section 不重算,仍用 marker 占位。
|
|
258
|
+
// 文本槽按 Agent 分区:同一挂载服务多个 Agent,交错装配时不能互相读到对方的正文。
|
|
259
|
+
const holders = section ? undefined : new WeakMap()
|
|
260
|
+
const sharedHolder = section ? undefined : { text: '' }
|
|
261
|
+
const holderOf = section ? undefined : (context) => {
|
|
262
|
+
const key = context?.agent
|
|
263
|
+
if (key === undefined || key === null) return sharedHolder
|
|
264
|
+
let holder = holders.get(key)
|
|
265
|
+
if (holder === undefined) { holder = { text: '' }; holders.set(key, holder) }
|
|
266
|
+
return holder
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
keep(systemPrompt[section ? 'section' : 'context']({
|
|
270
|
+
name: entryName,
|
|
271
|
+
order: base.order,
|
|
272
|
+
text: section ? marker : (context) => holderOf(context).text,
|
|
273
|
+
}), `${name}: conditional ${entryName}`)
|
|
274
|
+
} catch (error) { throw sourceError === undefined ? error : sourceError(error, base.sourceModuleId) }
|
|
275
|
+
bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts', holderOf })
|
|
257
276
|
ordered[section ? 'sections' : 'contexts'].push(orderedEntry(entryName, base))
|
|
258
277
|
}
|
|
259
278
|
let active = true
|
|
@@ -261,9 +280,13 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, o
|
|
|
261
280
|
return [on('system-prompt/assemble', async (assembly, context, next, invocation) => {
|
|
262
281
|
const frame = invocation ?? ruleFrame('system-prompt/assemble', [assembly, context], warnOnce)
|
|
263
282
|
for (const binding of bindings) {
|
|
264
|
-
const entry =
|
|
283
|
+
const entry = binding.collection === 'sections'
|
|
284
|
+
? assembly.sections.find(item => item.name === binding.name && item.text === marker)
|
|
285
|
+
: assembly.contexts.find(item => item.name === binding.name)
|
|
265
286
|
if (entry === undefined) continue
|
|
287
|
+
const holder = binding.holderOf === undefined ? undefined : binding.holderOf(context)
|
|
266
288
|
entry.text = ''
|
|
289
|
+
if (holder !== undefined) holder.text = ''
|
|
267
290
|
const blocks = []
|
|
268
291
|
for (const config of binding.group) {
|
|
269
292
|
if (!active || context.signal?.aborted || !actionMatches(config, frame) || !matchesAgentScope(config, context.agent)) continue
|
|
@@ -274,7 +297,10 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, o
|
|
|
274
297
|
if (text.length) blocks.push(text)
|
|
275
298
|
} catch (error) { warnOnce(`${name}: rule text ${config.id} failed: ${String(error?.message ?? error)}`) }
|
|
276
299
|
}
|
|
277
|
-
if (active && !context.signal?.aborted)
|
|
300
|
+
if (active && !context.signal?.aborted) {
|
|
301
|
+
entry.text = blocks.join('\n\n')
|
|
302
|
+
if (holder !== undefined) holder.text = entry.text
|
|
303
|
+
}
|
|
278
304
|
}
|
|
279
305
|
return next()
|
|
280
306
|
})]
|
|
@@ -282,7 +308,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, o
|
|
|
282
308
|
}
|
|
283
309
|
|
|
284
310
|
/** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
|
|
285
|
-
function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on, ordered) {
|
|
311
|
+
function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on, ordered, sourceError) {
|
|
286
312
|
const disposers = []
|
|
287
313
|
const systemPrompt = getService(ctx, 'systemPrompt')
|
|
288
314
|
if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
|
|
@@ -309,44 +335,55 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on, ordered
|
|
|
309
335
|
}), `${name}: context ${base.id}`)
|
|
310
336
|
ordered.contexts.push(orderedEntry(entryName, base))
|
|
311
337
|
} catch (error) {
|
|
338
|
+
if (sourceError !== undefined) throw sourceError(error, base.sourceModuleId)
|
|
312
339
|
if (base.linkedDelivery) throw error
|
|
313
340
|
warnOnce(`${name}: runtime-context config ${base.id} failed: ${String(error?.message ?? error)}`)
|
|
314
341
|
}
|
|
315
342
|
}
|
|
316
|
-
// 官方 provider
|
|
317
|
-
//
|
|
343
|
+
// 官方 provider 必须同步:注册函数 provider 读取 holder,再在 waterfall 中填充。
|
|
344
|
+
// 官方 refreshContext 会用注册 provider 重算 contexts;返回 holder 当前值,填充才不会被 reset。
|
|
318
345
|
const placeholders = configs.filter(needsResolver)
|
|
319
346
|
.sort(compareTextPlacement)
|
|
320
347
|
if (placeholders.length === 0) return disposers
|
|
321
|
-
const slotVariable = `pt_runtime_${newMessageId('context').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
|
|
322
|
-
const slotText = `{{${slotVariable}}}`
|
|
323
348
|
const registered = new Map()
|
|
324
349
|
let active = true
|
|
325
350
|
keep(() => { active = false }, `${name}: runtime-context lifecycle`)
|
|
326
|
-
keep(systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
|
|
327
351
|
for (const config of placeholders) {
|
|
328
352
|
try {
|
|
329
353
|
const contextName = textRegistrationName(config, 'contextName')
|
|
354
|
+
// 文本槽按 Agent 分区:交错装配时 A 的 refresh 不得读到 B 的正文。
|
|
355
|
+
const holders = new WeakMap()
|
|
356
|
+
const sharedHolder = { text: '' }
|
|
357
|
+
const holderOf = (context) => {
|
|
358
|
+
const key = context?.agent
|
|
359
|
+
if (key === undefined || key === null) return sharedHolder
|
|
360
|
+
let holder = holders.get(key)
|
|
361
|
+
if (holder === undefined) { holder = { text: '' }; holders.set(key, holder) }
|
|
362
|
+
return holder
|
|
363
|
+
}
|
|
330
364
|
keep(systemPrompt.context({
|
|
331
365
|
name: contextName,
|
|
332
366
|
order: config.order,
|
|
333
|
-
text:
|
|
367
|
+
text: (context) => holderOf(context).text,
|
|
334
368
|
}), `${name}: context ${config.id}`)
|
|
335
|
-
registered.set(contextName, config)
|
|
369
|
+
registered.set(contextName, { config, holderOf })
|
|
336
370
|
ordered.contexts.push(orderedEntry(contextName, config))
|
|
337
371
|
} catch (error) {
|
|
372
|
+
if (sourceError !== undefined) throw sourceError(error, config.sourceModuleId)
|
|
338
373
|
warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
|
|
339
374
|
}
|
|
340
375
|
}
|
|
341
376
|
disposers.push(on('system-prompt/assemble', async (assembly, context, next) => {
|
|
342
|
-
const entries = assembly.contexts.filter(entry => registered.has(entry.name)
|
|
377
|
+
const entries = assembly.contexts.filter(entry => registered.has(entry.name))
|
|
343
378
|
for (const entry of entries) {
|
|
344
|
-
const config = registered.get(entry.name)
|
|
379
|
+
const { config, holderOf } = registered.get(entry.name)
|
|
380
|
+
const holder = holderOf(context)
|
|
345
381
|
entry.text = ''
|
|
382
|
+
holder.text = ''
|
|
346
383
|
if (!active || context.signal?.aborted) continue
|
|
347
384
|
try {
|
|
348
385
|
const text = await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
|
|
349
|
-
if (active && !context.signal?.aborted) entry.text = text
|
|
386
|
+
if (active && !context.signal?.aborted) { entry.text = text; holder.text = text }
|
|
350
387
|
} catch (error) {
|
|
351
388
|
warnOnce(`${name}: runtime-context ${config.id} resolve failed: ${String(error?.message ?? error)}`)
|
|
352
389
|
}
|
|
@@ -681,9 +718,9 @@ export function wireLayers(ctx, configs, warnOnce, options = {}) {
|
|
|
681
718
|
&& (typeof config.rule?.when === 'function' || typeof config.actionWhen === 'function'
|
|
682
719
|
|| (config.rule !== undefined || config.params?.templateEvaluation === true) && config.params?.complete !== true && config.params?.suppressRuntimeContext !== true))
|
|
683
720
|
const regular = configs.filter(config => !conditional.includes(config))
|
|
684
|
-
wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep, ordered)
|
|
685
|
-
wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on, ordered)
|
|
686
|
-
wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on, ordered)
|
|
721
|
+
wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep, ordered, options.sourceError)
|
|
722
|
+
wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on, ordered, options.sourceError)
|
|
723
|
+
wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on, ordered, options.sourceError)
|
|
687
724
|
wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on, options.turnStopBudgets)
|
|
688
725
|
wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on)
|
|
689
726
|
// 官方 order 只提供初始槽位;configOrder 覆盖段/上下文的先后(平台段与其他插件段不动)。
|
package/engine/present-as.mjs
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* declared for this scope`)——本行不捕获、不降级,让它响亮失败(装配失败优于静默换形态)。
|
|
14
14
|
*
|
|
15
15
|
* `ptc` 需要 `ctx.ptcRuntime`(宿主 base bundle 的 `ptc-runtime` 行提供):用 `ctx.inject`
|
|
16
|
-
* 等它就绪再声明;等不到本行停在 pending
|
|
17
|
-
*
|
|
16
|
+
* 等它就绪再声明;等不到本行停在 pending。受管装配观察初始化结果,声明失败时按来源
|
|
17
|
+
* 回退或撤销模块;本行不自行改成 native。`native` 不需要运行时,直接声明。
|
|
18
18
|
*
|
|
19
19
|
* disposer:`presentAs` 本身就是 effect(它用调用方的 ctx 注册、并把那个 disposer 还回来),
|
|
20
20
|
* 本行不再包一层,随 fiber 释放即撤销。
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type RetrievalEvent = { readonly seq: number; readonly type: string }
|
|
2
|
+
export type SessionReference = { kind: 'session'; moduleId: string; sessionId: string; seqs: number[]; revision: string; start: number; end: number }
|
|
3
|
+
export const MAX_REFERENCE_CHARS: number
|
|
4
|
+
export function sessionReferenceText(events: readonly RetrievalEvent[], seqs: readonly number[]): string
|
|
5
|
+
export function makeSessionReference(moduleId: string, sessionId: string, events: readonly RetrievalEvent[], seqs: readonly number[], range?: { start?: number; end?: number }): SessionReference
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
export const MAX_REFERENCE_CHARS = 8192
|
|
4
|
+
|
|
5
|
+
/** seq 是持久日志身份;不排序为数值区间,也不以相同正文替代。 */
|
|
6
|
+
export function sessionReferenceText(events, seqs) {
|
|
7
|
+
if (!Array.isArray(seqs) || seqs.length === 0 || seqs.length > 2048 || new Set(seqs).size !== seqs.length || seqs.some(seq => !Number.isSafeInteger(seq) || seq < 0)) throw new Error('检索引用事件身份无效')
|
|
8
|
+
const bySeq = new Map(events.map(event => [event.seq, event]))
|
|
9
|
+
let bytes = 0
|
|
10
|
+
return seqs.map(seq => {
|
|
11
|
+
const event = bySeq.get(seq)
|
|
12
|
+
if (!event) throw new Error('检索引用已失效')
|
|
13
|
+
const text = JSON.stringify(event)
|
|
14
|
+
bytes += Buffer.byteLength(text)
|
|
15
|
+
if (bytes > 64 * 1024 * 1024) throw new Error('检索引用来源超过64 MiB上限')
|
|
16
|
+
return text
|
|
17
|
+
}).join('\n')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function makeSessionReference(moduleId, sessionId, events, seqs, range = {}) {
|
|
21
|
+
if (typeof moduleId !== 'string' || !moduleId || typeof sessionId !== 'string' || !sessionId) throw new Error('检索引用缺少真实来源身份')
|
|
22
|
+
const text = sessionReferenceText(events, seqs)
|
|
23
|
+
const start = range.start ?? 0, end = range.end ?? Math.min(text.length, start + MAX_REFERENCE_CHARS)
|
|
24
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end <= start || end > text.length || end - start > MAX_REFERENCE_CHARS) throw new Error('检索引用范围无效')
|
|
25
|
+
return { kind: 'session', moduleId, sessionId, seqs: [...seqs], revision: createHash('sha256').update(text).digest('hex'), start, end }
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { bool, defineConfig, stringList } from './fields.mjs'
|
|
2
|
+
import { toolServiceName } from './service-names.mjs'
|
|
3
|
+
|
|
4
|
+
export const name = 'retrieval'
|
|
5
|
+
export const inject = ['tools']
|
|
6
|
+
export const configContract = defineConfig({
|
|
7
|
+
enabled: bool({ default: false }),
|
|
8
|
+
paths: stringList({ default: () => [], allowEmpty: true, dedupe: true }),
|
|
9
|
+
sessions: stringList({ default: () => [], allowEmpty: true, dedupe: true }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export function apply(ctx, raw, options) {
|
|
13
|
+
const config = configContract.parse(raw, name)
|
|
14
|
+
if (!config.enabled) return
|
|
15
|
+
const fiber = ctx.inject([toolServiceName(name)], scopeCtx => scopeCtx.effect(() => scopeCtx.get(toolServiceName(name)).mount(scopeCtx, config, options)))
|
|
16
|
+
return () => { void fiber.dispose() }
|
|
17
|
+
}
|
package/engine/rule-engine.mjs
CHANGED
|
@@ -37,5 +37,6 @@ export async function apply(ctx, config) {
|
|
|
37
37
|
const moduleId = typeof packet.moduleId === 'string' ? packet.moduleId : basename(dirname(file))
|
|
38
38
|
const rules = compileRules(packet.rules, { moduleId, variables: packet.variables, variablesEnabled: packet.variablesEnabled, configOrder: packet.configOrder, promptConfigOptions: { templateBaseUrl, templateModuleRoot, strategyDir } })
|
|
39
39
|
// 条件在挂载缝绑定(preset 需要挂载 scope 的 ctx);官方模块导出只作可选兜底。
|
|
40
|
+
// 独立装配路径没有插件侧服务,manifest 条件在这个入口恒不可用(见 docs/engine-reuse.md)。
|
|
40
41
|
return mountRuleSources(ctx, [{ moduleId, rules }], { standingMountFor: await loadStandingMountFor() })
|
|
41
42
|
}
|
|
@@ -11,6 +11,11 @@ export interface RuleVariableStore {
|
|
|
11
11
|
publish(moduleId: string): void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/** 清单检查事实通道(宿主读取器);引擎只调用 read,undefined 表示不可用。 */
|
|
15
|
+
export interface RuleManifestCheck {
|
|
16
|
+
read(moduleId: string): boolean | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
/** 判定上报载荷;仅覆盖插件依赖面(宿主按 `RuleOutcomeReport` 消费它)。 */
|
|
15
20
|
export interface RuleOutcomeReport {
|
|
16
21
|
moduleId?: string
|
|
@@ -30,6 +35,8 @@ export interface MountRuleSourcesOptions {
|
|
|
30
35
|
plugin?: string
|
|
31
36
|
warnOnce?: WarnOnce
|
|
32
37
|
variableStore?: RuleVariableStore
|
|
38
|
+
/** 清单 checks 事实;缺省=manifest 条件恒不可用。 */
|
|
39
|
+
manifestCheck?: RuleManifestCheck
|
|
33
40
|
/** 官方预设兜底;缺省=没有兜底(预设条件恒不命中)。 */
|
|
34
41
|
standingMountFor?: StandingMountFor
|
|
35
42
|
/** 注入的晋升相位实例对;缺省从协调器或自建池取。 */
|
|
@@ -38,7 +45,7 @@ export interface MountRuleSourcesOptions {
|
|
|
38
45
|
onOutcome?(report: RuleOutcomeReport): void
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
/**
|
|
48
|
+
/** 所有规则来源的唯一装配入口;返回 disposer。可归因挂载错误携带 sourceModuleId,原错误保留在 cause。 */
|
|
42
49
|
export function mountRuleSources(
|
|
43
50
|
ctx: unknown,
|
|
44
51
|
sources: ReadonlyArray<RuleSourceLike>,
|
package/engine/rule-runtime.mjs
CHANGED
|
@@ -16,6 +16,8 @@ const noNext = new Set(['agent/inbox/inserted', 'agent/turn-stopping', 'subagent
|
|
|
16
16
|
const compare = (a, b) => a.action.channelOrder - b.action.channelOrder
|
|
17
17
|
|| a.moduleId.localeCompare(b.moduleId) || a.ruleIndex - b.ruleIndex || a.action.actionIndex - b.action.actionIndex
|
|
18
18
|
|
|
19
|
+
const sourceError = (error, moduleId) => Object.assign(new Error(String(error?.message ?? error), { cause: error }), { sourceModuleId: moduleId })
|
|
20
|
+
|
|
19
21
|
/** 所有来源只进入这个装配入口;受众限定固定效果要求 ctx 属于 options.agent 的 scope。 */
|
|
20
22
|
export function mountRuleSources(ctx, sources, options = {}) {
|
|
21
23
|
validateDeliverySources(sources)
|
|
@@ -32,7 +34,6 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
32
34
|
// 条件绑定**在这里**发生:`compileRules` 编译期没有挂载 scope 的 ctx(preset 条件在那边恒
|
|
33
35
|
// UNAVAILABLE 且静默)。必须早于事件订阅与动作注册——动作级 `actionWhen.observe` 是订阅通道,
|
|
34
36
|
// 绑晚了那一类谓词会漏事件。`standingMountFor` 是可选兜底(官方包缺席=没有兜底,不是错误)。
|
|
35
|
-
rebindConditions(sources, { ctx, standingMountFor: options.standingMountFor, promotionPool: pool, variableStore: options.variableStore })
|
|
36
37
|
const warnOnce = options.warnOnce ?? createWarnOnce(ctx, options.plugin ?? 'rule-engine')
|
|
37
38
|
// 非 pre-step 动作仍共享一次 ruleFrame;规则实例没有来源字段(RuleDefinition 不暴露 moduleId),
|
|
38
39
|
// 因此保留一份局部 rule→module 映射。它不是 pre-step 来源事实的重建,不参与 pre-step 的 source sink。
|
|
@@ -50,19 +51,23 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
50
51
|
// 因此按 moduleId 分档;来源 id 缺失时落空串兜底键(与 wireTurnStops 同源)。
|
|
51
52
|
const turnStopBudgets = new Map()
|
|
52
53
|
const injections = new Map(sources.map(source => [source.moduleId, { moduleId: source.moduleId, sourceId: moduleSourceId(source.moduleId), configs: [], ruleActions: [] }]))
|
|
53
|
-
const rules = sources.flatMap(source => source.rules.filter(rule => rule.enabled !== false))
|
|
54
54
|
// 动作级分支条件(actionWhen)也要喂 session/event:否则 else / 嵌套 if 里的 phase / count
|
|
55
55
|
// 谓词永远收不到事件,状态停在冷扫那一刻(规则级 when 已由 rules 覆盖,动作级单独补齐)。
|
|
56
|
-
const observer = wireTriggerObservers(ctx, [
|
|
57
|
-
...rules,
|
|
58
|
-
...rules.flatMap(rule => (rule.actions ?? [])
|
|
59
|
-
.filter(action => typeof action.actionWhen?.observe === 'function')
|
|
60
|
-
.map(action => ({ id: action.id, when: action.actionWhen }))),
|
|
61
|
-
], { plugin: options.plugin ?? 'rule-engine', warnOnce })
|
|
62
|
-
if (observer) releases.push(observer)
|
|
63
56
|
let active = true
|
|
64
57
|
const bind = (item, on) => prepareAction(item.action, { plugin: options.plugin, warnOnce, promptConfigOptions: item.rule.promptConfigOptions, on })(ctx)
|
|
65
58
|
try {
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
try { rebindConditions([source], { ctx, standingMountFor: options.standingMountFor, promotionPool: pool, variableStore: options.variableStore, manifestCheck: options.manifestCheck }) }
|
|
61
|
+
catch (error) { throw sourceError(error, source.moduleId) }
|
|
62
|
+
}
|
|
63
|
+
const rules = sources.flatMap(source => source.rules.filter(rule => rule.enabled !== false))
|
|
64
|
+
const observer = wireTriggerObservers(ctx, [
|
|
65
|
+
...rules,
|
|
66
|
+
...rules.flatMap(rule => (rule.actions ?? [])
|
|
67
|
+
.filter(action => typeof action.actionWhen?.observe === 'function')
|
|
68
|
+
.map(action => ({ id: action.id, when: action.actionWhen }))),
|
|
69
|
+
], { plugin: options.plugin ?? 'rule-engine', warnOnce })
|
|
70
|
+
if (observer) releases.push(observer)
|
|
66
71
|
for (const source of sources) for (const [ruleIndex, rule] of source.rules.entries()) {
|
|
67
72
|
if (rule.enabled === false) continue
|
|
68
73
|
for (const action of rule.actions) {
|
|
@@ -82,6 +87,7 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
82
87
|
releases.push(wireLayers(ctx, textConfigs, warnOnce, {
|
|
83
88
|
agent: options.agent,
|
|
84
89
|
turnStopBudgets,
|
|
90
|
+
sourceError,
|
|
85
91
|
on: (channel, handler) => {
|
|
86
92
|
if (channel !== point.channel) throw new TypeError(`rule action registered outside ${point.channel}: ${channel}`)
|
|
87
93
|
handlers.push({ handler }); return () => {}
|
|
@@ -109,7 +115,9 @@ export function mountRuleSources(ctx, sources, options = {}) {
|
|
|
109
115
|
// 这里再返回一个真 disposer 会与它重复释放同一批 handler。
|
|
110
116
|
return () => {}
|
|
111
117
|
}
|
|
112
|
-
|
|
118
|
+
let dispose
|
|
119
|
+
try { dispose = bind(item, on) }
|
|
120
|
+
catch (error) { throw sourceError(error, moduleId) }
|
|
113
121
|
// 未命中通知:条件为假时动作不会进入 handler,撤销型动作(tool-visibility 的执行闸)
|
|
114
122
|
// 只能靠这条回调收手——否则第一轮为真后永久留闸。
|
|
115
123
|
if (typeof dispose?.miss === 'function') {
|
package/engine/rule-spec.mjs
CHANGED
|
@@ -231,8 +231,8 @@ export function compileRules(specs, options = {}) {
|
|
|
231
231
|
if (!Array.isArray(specs)) throw new TypeError('rules must be an array')
|
|
232
232
|
// 条件的绑定**不在编译期**:`preset` 谓词的取法要挂载 scope 的 ctx(`conditions/preset.mjs`),
|
|
233
233
|
// 准备期拿不到。这里显式剥掉挂载期选项——传了也不生效,免得调用方以为已经绑好。
|
|
234
|
-
// variableStore 与 ctx/standingMountFor 同理:绑定发生在挂载期,准备期传了也不生效。
|
|
235
|
-
const { ctx: _ctx, standingMountFor: _standingMountFor, variableStore: _variableStore, ...conditionOptions } = options
|
|
234
|
+
// variableStore / manifestCheck 与 ctx/standingMountFor 同理:绑定发生在挂载期,准备期传了也不生效。
|
|
235
|
+
const { ctx: _ctx, standingMountFor: _standingMountFor, variableStore: _variableStore, manifestCheck: _manifestCheck, ...conditionOptions } = options
|
|
236
236
|
const ids = new Set()
|
|
237
237
|
const pendingConfigs = []
|
|
238
238
|
const moduleId = options.moduleId ?? options.sourceModuleId
|
|
@@ -411,6 +411,8 @@ function needsMountContext(node) {
|
|
|
411
411
|
if (Object.hasOwn(node, 'preset')) return true
|
|
412
412
|
// variable 要读挂载 scope 的变量适配器(准备期没有)。
|
|
413
413
|
if (Object.hasOwn(node, 'variable')) return true
|
|
414
|
+
// manifest 要读挂载 scope 的清单检查服务(准备期没有)。
|
|
415
|
+
if (Object.hasOwn(node, 'manifest')) return true
|
|
414
416
|
if (Object.hasOwn(node, 'phase') && node.phase?.subscribe !== false) return true
|
|
415
417
|
if (node.anchor?.after !== undefined) return true
|
|
416
418
|
return Object.values(node).some(needsMountContext)
|