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.
Files changed (50) hide show
  1. package/README.md +17 -1
  2. package/engine/actions/index.mjs +5 -3
  3. package/engine/actions/tool-visibility.mjs +4 -1
  4. package/engine/compositions/source/local/context-provenance.yml +9 -0
  5. package/engine/compositions/source/local/generation-recovery.yml +10 -0
  6. package/engine/compositions/source/local/retrieval.yml +7 -0
  7. package/engine/compositions/source/local/soul-ledger.yml +3 -0
  8. package/engine/compositions/source/local/soul-maintenance.yml +9 -0
  9. package/engine/conditions/examples.mjs +1 -0
  10. package/engine/conditions/index.mjs +7 -0
  11. package/engine/conditions/manifest.mjs +28 -0
  12. package/engine/context-idle-compaction.mjs +58 -0
  13. package/engine/context-provenance.d.mts +7 -0
  14. package/engine/context-provenance.mjs +156 -0
  15. package/engine/context-pruner.mjs +64 -0
  16. package/engine/context-routing.mjs +45 -0
  17. package/engine/context-summary.mjs +80 -0
  18. package/engine/fillers.mjs +11 -2
  19. package/engine/generation-recovery-coordinator.mjs +40 -0
  20. package/engine/generation-recovery-maintenance.mjs +18 -0
  21. package/engine/generation-recovery.mjs +153 -0
  22. package/engine/host-package.mjs +13 -0
  23. package/engine/layers.d.mts +4 -0
  24. package/engine/layers.mjs +57 -20
  25. package/engine/present-as.mjs +2 -2
  26. package/engine/retrieval-reference.d.mts +5 -0
  27. package/engine/retrieval-reference.mjs +26 -0
  28. package/engine/retrieval.mjs +17 -0
  29. package/engine/rule-engine.mjs +1 -0
  30. package/engine/rule-runtime.d.mts +8 -1
  31. package/engine/rule-runtime.mjs +18 -10
  32. package/engine/rule-spec.mjs +4 -2
  33. package/engine/schema.mjs +1 -1
  34. package/engine/service-names.d.mts +5 -0
  35. package/engine/service-names.mjs +5 -0
  36. package/engine/soul-ledger.mjs +8 -0
  37. package/engine/soul-maintenance.mjs +20 -0
  38. package/lib/client.js +12 -2
  39. package/lib/client.js.map +1 -1
  40. package/lib/index.mjs +1868 -208
  41. package/modules/soul/assets/ledger.md +6 -2
  42. package/modules/soul/assets/manifest.yml +2 -0
  43. package/modules/soul/assets/templates/agents-template.md +3 -0
  44. package/modules/soul/assets/templates/self-template.md +19 -0
  45. package/modules/soul/assets/tools.md +10 -0
  46. package/modules/soul/module.yml +89 -15
  47. package/modules/tool-surface/module.yml +28 -6
  48. package/package.json +1 -1
  49. package/skills/dsh-module/reference.md +1 -1
  50. package/templates/80-tool-surface.yml +25 -4
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * fillers — 引擎内置的动态填充器(strategy=placeholder 的数据源)。
3
- * 全部遵循同一铁律:服务缺失或列表失败时返回 null(跳过该配置),绝不伤及会话。
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
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import type { UserMessage } from '@deepseek-ai/dsh-llm'
2
+
3
+ /** 已有提示词引擎通知消息;使用宿主当前的插件来源契约。 */
4
+ export function pluginMessage(prefix: string, text: string, summary: string): UserMessage
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
- keep(systemPrompt[section ? 'section' : 'context']({ name: entryName, order: base.order, text: marker }), `${name}: conditional ${entryName}`)
256
- bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts' })
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 = assembly[binding.collection].find(item => item.name === binding.name && item.text === marker)
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) entry.text = blocks.join('\n\n')
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 必须同步:先注册可渲染为空的私有变量占位,再在 waterfall 中填充。
317
- // 标记随官方排序/遮蔽进入本次 assembly,无会话缓存,也不依赖调用方 context 的对象身份。
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: slotText,
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) && entry.text === slotText)
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 覆盖段/上下文的先后(平台段与其他插件段不动)。
@@ -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
- * 退回 native,否则「开了 PTC」的部署会悄悄以 native 跑。`native` 不需要运行时,直接声明。
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
+ }
@@ -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
- /** 所有规则来源的唯一装配入口;返回释放本次挂载的 disposer。 */
48
+ /** 所有规则来源的唯一装配入口;返回 disposer。可归因挂载错误携带 sourceModuleId,原错误保留在 cause。 */
42
49
  export function mountRuleSources(
43
50
  ctx: unknown,
44
51
  sources: ReadonlyArray<RuleSourceLike>,
@@ -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
- const dispose = bind(item, on)
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') {
@@ -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)