dsh-plugin-prompt-tool 3.1.4 → 3.1.5

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 CHANGED
@@ -112,7 +112,7 @@ persona:
112
112
  | module-file-tools | module_file_read / module_file_append |
113
113
  | module-manifest-tools | module_manifest |
114
114
  | module-import-tools | module_import |
115
- | soul-ledger | soul_ledger |
115
+ | soul-ledger | soul_ledger / soul_state |
116
116
  | retrieval | module_search / module_open |
117
117
  | tool-config-engine | customTools 中的实际工具 |
118
118
 
@@ -134,11 +134,17 @@ outlet/vectorized/triggers 不能等价执行,相关规则禁用并报告;
134
134
 
135
135
  Soul 模块包含结构化账本、授权文件/历史检索、每步动态 SELF、上下文压缩与裁剪溯源、生成恢复和维护心跳。SELF 由 `rule-engine` 的内置动态内容源提供(`strategy: placeholder`、`fill: soul-self`),无需模块附带策略 JS。先用上述初始化预览补齐 `SELF.src.md` 骨架;已有模块副本须显式更新,包升级不会覆盖用户定义或私有数据,旧副本仍可使用自身保留的自定义策略。
136
136
 
137
- `soul_ledger` 自动署名真实会话,`lead` 需宿主审批,只有最后主导会话可追加 `promise`;通用文件追加拒绝新旧账本名。`module_search` 返回的引用交给 `module_open` 回取,历史会话只读取声明的白名单。SELF 的当前身份在每步读取时加入,维护默认每分钟只更新索引与快照,`wake` 独立且默认关闭。
137
+ `soul_ledger` 自动署名真实会话;`lead` 需宿主审批,`promise` 仅当前主导会话追加/收口,`shelved` 可由原作者或当前主导收口。`close` 通过稳定 `targetId` 追加完成/取消及依据,保留历史原行;`list` 的 `pending/open/pending-review` 视图提供版本分页。升级前历史事项标为待核对,不猜测已完成。
138
+
139
+ SELF 每步读取当前骨架、账本投影和身份/时钟,维护关闭或诊断快照陈旧也能呈现;索引只报告最近成功观测,不逐步扫描历史。`soul_state status/mark/check` 提供当前会话状态、带版本的短标记及只读运行诊断。未结提醒默认仅主会话,在集合变化或成功压缩后实际投递一次;候选装配不消费提醒。Soul 显式开启 45/60/80% 水位提示,只提示,不自动压缩。
140
+
141
+ `module_search` 支持全半角/大小写/千分位归一化、Soul 声明的有界近义表及 `mode: timeline` 时间线;命中标注方式,引用交给 `module_open` 重验授权与原文。历史会话仍只读白名单。维护默认每分钟更新索引与诊断快照,等待可取消,`wake` 独立且默认关闭。
138
142
 
139
143
  `generation-recovery` 的通用默认只截断;Soul 显式启用空回合检测和有界恢复,主会话额度 3、子代理 1,每会话独立且重挂不清零。维护压缩和扩大范围默认关闭。配置与边界见 [引擎运行时](docs/engine-reuse.md#重复输出安全截断)、[账本与SELF](docs/architecture-params.md#soul账本与派生快照)。
140
144
 
141
- 这些运行能力由本插件实现,不需要源项目 `dsh-agent-soul` 的仓库、插件或后台服务;上下文治理仍要求宿主提供[官方能力包](docs/engine-reuse.md#上下文运行时与溯源)(按包名白名单从 DSH 自带包解析,运行期不比对版本)。本模块不等价于源项目的全部功能:检索采用授权范围内的字面匹配,未提供源近义检索、上下文水位提醒、完整 doctor/诊断工具或附属面板;恢复判据与额度策略也有差异。`assets/reference/` 保留的源文档用于背景参考,其中的源 CLI 和第三方工具不表示本插件已提供对应能力。
145
+ 这些运行能力由本插件实现,不需要源项目 `dsh-agent-soul` 的仓库、插件或后台服务;上下文治理仍要求宿主提供[官方能力包](docs/engine-reuse.md#上下文运行时与溯源)。未提供向量库、规模持久索引、压缩帧直取、附属面板、源安装体系或恢复补额/惩罚。`assets/reference/` 保留源背景文档,其 CLI 不代表本插件接口。
146
+
147
+ 已有 Soul 定义通过 `soul_state check` 显式传 `includeUpgrade: true` 查看定向升级候选及冲突;完整 `files` 含必要配置、近义词表和现有可交换资产,再交 `module_import` 模型工具预览确认及重建,该入口保留私有数据、记忆和技能资产。包内种子不覆盖用户副本。包含 `close` 的账本要求支持账本 v2 的程序:旧版会忽略收口且仍能写入,回滚前必须撤写能力及旧 SELF/提醒贡献,不能删除 close 行迁就旧版。
142
148
 
143
149
  上下文能力按包名白名单从 DSH 自带包解析,不增加插件依赖,运行期不校验版本;仅包缺失或未登记时拒绝该能力装配。包代码更新需要用户重启 DSH 加载;加载后修改模块启停/参数沿运行时刷新,不改官方 preset 或 profile patch。
144
150
 
@@ -23,3 +23,4 @@ export interface EpochPromotionOptions {
23
23
 
24
24
  /** 建一份相位追踪器;`promoteEvents` 是 durable 事件类型集合(见 `shared.mjs#PROMOTE_EVENTS`)。 */
25
25
  export function createEpochPromotion(promoteEvents: Iterable<string>, options?: EpochPromotionOptions): PromotionTracker
26
+ export function isSuccessfulCompactionEnd(event: unknown): boolean
@@ -35,21 +35,26 @@ export function composite(node) {
35
35
  const children = list.map(composite)
36
36
  const predicates = {
37
37
  [MATCH_LOGIC.ANY]: (payload) => {
38
+ predicate.soulReminders = []
38
39
  let unknown = false
39
40
  for (const child of children) {
40
41
  const result = child(payload)
41
42
  if (result === UNAVAILABLE) unknown = true
42
- else if (result) return true
43
+ else if (result) { predicate.soulReminders = child.soulReminders ?? []; return true }
43
44
  }
44
45
  return unknown ? UNAVAILABLE : false
45
46
  },
46
47
  [MATCH_LOGIC.ALL]: (payload) => {
48
+ predicate.soulReminders = []
49
+ const reminders = []
47
50
  let unknown = false
48
51
  for (const child of children) {
49
52
  const result = child(payload)
50
53
  if (result === UNAVAILABLE) unknown = true
51
54
  else if (!result) return false
55
+ if (result === true) reminders.push(...(child.soulReminders ?? []))
52
56
  }
57
+ if (!unknown) predicate.soulReminders = reminders
53
58
  return unknown ? UNAVAILABLE : true
54
59
  },
55
60
  [MATCH_LOGIC.NOT_ANY]: (payload) => {
@@ -9,7 +9,7 @@ import { UNAVAILABLE } from './availability.mjs'
9
9
  * 缺省不传即零开销,也不改变任何判定结果。
10
10
  */
11
11
  export function ruleFrame(channel, args, warnOnce = () => {}, ctx, onOutcome) {
12
- return { channel, args, subject: subjectOf(channel, args, warnOnce, ctx), decisions: new Map(), actionDecisions: new Set(), warnOnce, onOutcome }
12
+ return { channel, args, subject: subjectOf(channel, args, warnOnce, ctx), decisions: new Map(), actionDecisions: new Set(), reminders: new Map(), warnOnce, onOutcome }
13
13
  }
14
14
 
15
15
  /** 条件返回值的三值归一:词表与 bridge 的 `RuleOutcomeKind` 共享,故只此一处定义。 */
@@ -42,7 +42,7 @@ function decideRule(rule, frame) {
42
42
  frame.warnOnce(`rule ${rule.id} condition failed: ${String(error?.message ?? error)}`)
43
43
  }
44
44
  }
45
- const decided = { hit, outcome }
45
+ const decided = { hit, outcome, reminders: hit ? rule.when?.soulReminders ?? [] : [] }
46
46
  frame.decisions.set(rule, decided)
47
47
  return decided
48
48
  }
@@ -70,7 +70,11 @@ export function ruleMatches(rule, frame) {
70
70
  */
71
71
  export function actionMatches(entry, frame) {
72
72
  const branched = entry.actionWhen !== undefined || entry.bypassRuleWhen === true
73
- if (!branched) return ruleMatches(entry.rule, frame)
73
+ if (!branched) {
74
+ const hit = ruleMatches(entry.rule, frame)
75
+ frame.reminders.set(entry, hit ? decideRule(entry.rule, frame).reminders ?? [] : [])
76
+ return hit
77
+ }
74
78
  // `else`(bypassRuleWhen)自带 not(if),不叠加规则级判定;其余带分支的动作自判
75
79
  // 规则级(不发上报),判定仍走同一份帧内缓存,同帧同规则依旧只判一次。
76
80
  const ruled = entry.bypassRuleWhen === true ? { hit: true, outcome: 'hit' } : decideRule(entry.rule, frame)
@@ -92,5 +96,20 @@ export function actionMatches(entry, frame) {
92
96
  frame.actionDecisions.add(entry)
93
97
  report(frame, entry.rule, outcome)
94
98
  }
99
+ frame.reminders.set(entry, hit ? [...(ruled.reminders ?? []), ...(entry.actionWhen?.soulReminders ?? [])] : [])
95
100
  return hit
96
101
  }
102
+
103
+ /** 只关联实际命中路径的正向Soul事实;普通分支没有提醒戳。 */
104
+ export function actionSoulReminder(entry, frame) {
105
+ return frame.reminders.get(entry)?.[0]
106
+ }
107
+
108
+ export function recheckSoulReminder(entry, frame, expected) {
109
+ if (expected === undefined) return true
110
+ const current = ruleFrame(frame.channel, frame.args, frame.warnOnce, undefined)
111
+ if (!actionMatches(entry, current)) return false
112
+ const fact = actionSoulReminder(entry, current)
113
+ return fact?.predicate === expected.predicate && fact.moduleId === expected.moduleId
114
+ && fact.pendingVersion === expected.pendingVersion && fact.epoch === expected.epoch
115
+ }
@@ -10,4 +10,5 @@ export const PREDICATE_EXAMPLES = {
10
10
  anchor: { subject: 'assistantReasoning', keys: ['确认'], mode: 'prefix' },
11
11
  variable: { key: 'quota', op: 'lte', value: '0', scope: 'effective' },
12
12
  manifest: { ok: true },
13
+ soul: { pending: true },
13
14
  }
@@ -9,6 +9,7 @@ import { createScopePredicate } from './scope.mjs'
9
9
  import { createAnchorPredicate } from './anchor.mjs'
10
10
  import { createVariablePredicate } from './variable.mjs'
11
11
  import { createManifestPredicate } from './manifest.mjs'
12
+ import { createSoulPredicate } from './soul.mjs'
12
13
  import { withAvailableInput } from './availability.mjs'
13
14
  import { composite } from './composite.mjs'
14
15
  import { COMPOSITE_OPERATORS } from './operators.mjs'
@@ -23,6 +24,7 @@ export * from './scope.mjs'
23
24
  export * from './anchor.mjs'
24
25
  export * from './variable.mjs'
25
26
  export * from './manifest.mjs'
27
+ export * from './soul.mjs'
26
28
  export { subjectOf, channelTextSubjects, channelFactSubjects, FACT_PREDICATE_SUBJECTS } from './subject.mjs'
27
29
  export { composite } from './composite.mjs'
28
30
  export const PREDICATE_FACTORIES = Object.freeze({
@@ -37,6 +39,7 @@ export const PREDICATE_FACTORIES = Object.freeze({
37
39
  anchor: createAnchorPredicate,
38
40
  variable: createVariablePredicate,
39
41
  manifest: createManifestPredicate,
42
+ soul: createSoulPredicate,
40
43
  })
41
44
  export { COMPOSITE_OPERATORS }
42
45
 
@@ -102,5 +105,6 @@ export function compileWhen(node, context = {}) {
102
105
  if (key === 'manifest') {
103
106
  return createManifestPredicate(options, { manifestCheck: context.manifestCheck, moduleId: context.moduleId })
104
107
  }
108
+ if (key === 'soul') return createSoulPredicate(options, context)
105
109
  return withAvailableInput(key, options, factory(options))
106
110
  }
@@ -0,0 +1,15 @@
1
+ import { UNAVAILABLE } from './availability.mjs'
2
+ import { agentOf } from './subject.mjs'
3
+
4
+ export function createSoulPredicate(options = {}, context = {}) {
5
+ if (Object.keys(options).some(key => key !== 'pending') || options.pending !== true) throw new TypeError('predicates: soul requires pending: true')
6
+ const predicate = payload => {
7
+ predicate.soulReminders = []
8
+ if (typeof context.soulReminder?.read !== 'function' || !context.moduleId) return UNAVAILABLE
9
+ const fact = context.soulReminder.read(context.moduleId, agentOf(payload))
10
+ if (fact?.due === true) predicate.soulReminders = [{ ...fact, predicate, moduleId: context.moduleId }]
11
+ return typeof fact?.due === 'boolean' ? fact.due : UNAVAILABLE
12
+ }
13
+ predicate.kind = 'soul'
14
+ return predicate
15
+ }
@@ -1,2 +1,3 @@
1
1
  /** 宿主使用的投递身份公开面。 */
2
2
  export function identityOf(config: Record<string, unknown>): string
3
+ export function eventMessage(event: unknown): { source: Record<string, unknown> } | undefined
@@ -30,7 +30,7 @@ import { wireLayers } from './layers.mjs'
30
30
  import { renderTemplate } from './template-frame.mjs'
31
31
  import { selectContextActivation } from './context-activation.mjs'
32
32
  import { compareConfigSequence } from './order.mjs'
33
- import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
33
+ import { ruleFrame, actionMatches, actionSoulReminder, recheckSoulReminder } from './conditions/evaluation.mjs'
34
34
  import { composePreStepSources, promotionOwnerOf } from './pre-step-source.mjs'
35
35
  // 标记键名与「已登记进 config 白名单」同源(schema.mjs),此处不另写字面量。
36
36
  import { SESSION_VARIABLES_DISABLED } from './schema.mjs'
@@ -329,12 +329,15 @@ async function runPromptConfigBatch(options) {
329
329
  continue
330
330
  }
331
331
 
332
+ const reminder = actionSoulReminder(config, options.ruleFrame)
332
333
  const signal = options.payload?.signal
333
334
  if (signal?.aborted) continue
334
335
  const resolved = await config.resolve({ ctx, agent, session, signal, decision, messages, contextActivationSelected: scope.contextActivation.has(config) })
335
336
  if (signal?.aborted) continue
336
337
  if (resolved === null || resolved === undefined) continue
338
+ if (!recheckSoulReminder(config, options.ruleFrame, reminder)) continue
337
339
  const patched = { ...resolved }
340
+ if (reminder !== undefined) patched.source = { ...patched.source, soulReminder: { pendingVersion: reminder.pendingVersion, epoch: reminder.epoch } }
338
341
  // 按来源读取最新持久值;无变量正文不因另一个坏候选的读盘失败撤回。
339
342
  const mergedVars = {
340
343
  ...(config[SESSION_VARIABLES_DISABLED] === true ? {} : config.params?.templateEvaluation !== true && [patched.text, ...config.texts].some(text => typeof text === 'string' && text.includes('{{'))
@@ -191,7 +191,8 @@ function createSoulSelfResolver(config) {
191
191
  return ({ ctx, agent, signal }) => {
192
192
  const service = getService(ctx, SOUL_SELF_SERVICE)
193
193
  if (typeof service?.render !== 'function') throw new Error('SELF dependency missing')
194
- return { text: service.render(config.sourceModuleId, agent, signal) }
194
+ const text = service.render(config.sourceModuleId, agent, signal)
195
+ return typeof text?.then === 'function' ? text.then(value => ({ text: value })) : { text }
195
196
  }
196
197
  }
197
198
 
@@ -0,0 +1,6 @@
1
+ export interface RecoveryObservation { state: string; observedAt: string }
2
+ export function recoveryObservationOf(agent: object): RecoveryObservation | undefined
3
+ export const name: string
4
+ export const inject: string[]
5
+ export const configContract: { parse(raw: unknown, plugin: string): Record<string, unknown> }
6
+ export function apply(ctx: unknown, raw: unknown, options?: Record<string, unknown>): (() => void) | undefined
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto'
1
2
  import { bool, defineConfig, enumOf, int, passthrough } from './fields.mjs'
2
3
  import { capabilityAudience } from './agent-capability.mjs'
3
4
  import { recoveryCoordinator } from './generation-recovery-coordinator.mjs'
@@ -6,6 +7,8 @@ import { recoveryMaintenance } from './generation-recovery-maintenance.mjs'
6
7
 
7
8
  export const name = 'generation-recovery'
8
9
  export const inject = ['llm']
10
+ const observations = new WeakMap()
11
+ export const recoveryObservationOf = agent => observations.get(agent)
9
12
 
10
13
  const recoveryLimit = passthrough((value, plugin, field) => {
11
14
  if (value === undefined) return 0
@@ -19,6 +22,8 @@ export const configContract = defineConfig({
19
22
  textRepeatChars: int({ min: 3, default: 4096 }),
20
23
  reasoningRepeatChars: int({ min: 3, default: 8192 }),
21
24
  silentReasoningChars: int({ min: 0, default: 0 }),
25
+ lineRepeatChars: int({ min: 0, default: 0 }),
26
+ crossCallRepeatChars: int({ min: 0, default: 0 }),
22
27
  recoverEmpty: bool({ default: false }),
23
28
  mainRecoveryLimit: recoveryLimit,
24
29
  subagentRecoveryLimit: recoveryLimit,
@@ -54,20 +59,53 @@ function repeatDetector(threshold, maxCycle) {
54
59
  }
55
60
  }
56
61
 
62
+ // 只保存行摘要;窗口与单行都有上限,引用与代码围栏不计入材料判据。
63
+ function materialDetector(config, previous) {
64
+ const counts = new Map()
65
+ let line = '', fenced = false, overflow = false, chars = 0, lines = 0, repeated = 0, dominant = 0
66
+ const overlap = [0, 0]
67
+ return {
68
+ push(text) {
69
+ let offset = 0
70
+ for (const char of text) {
71
+ offset += char.length
72
+ if (char !== '\n') { if (line.length < 1024) line += char; else overflow = true; continue }
73
+ const value = line.trim(); line = ''
74
+ if (value.startsWith('```') || value.startsWith('~~~')) { fenced = !fenced; overflow = false; continue }
75
+ if (overflow || fenced || value.startsWith('>') || value.length < 12 || lines >= 128) { overflow = false; continue }
76
+ const key = createHash('sha256').update(value).digest('hex')
77
+ const count = (counts.get(key) ?? 0) + 1
78
+ counts.set(key, count); lines++; chars += value.length
79
+ if (count > 1) repeated += value.length
80
+ dominant = Math.max(dominant, count)
81
+ previous.forEach((sample, index) => { if (sample.has(key)) overlap[index] += value.length })
82
+ if (config.lineRepeatChars > 0 && chars >= config.lineRepeatChars && lines >= 6 && repeated / chars >= 0.8 && dominant / lines >= 0.5) return { offset, cause: 'repeated-lines' }
83
+ if (config.crossCallRepeatChars > 0 && chars >= config.crossCallRepeatChars && lines >= 6 && previous.length === 2 && overlap.every(value => value / chars >= 0.9)) return { offset, cause: 'repeated-material' }
84
+ }
85
+ },
86
+ sample: () => lines >= 6 && chars >= config.crossCallRepeatChars ? new Set(counts.keys()) : undefined,
87
+ }
88
+ }
89
+
57
90
  export function apply(ctx, raw, options = {}) {
58
91
  const { agent } = options
59
92
  const config = configContract.parse(raw, name)
60
93
  if (!config.enabled || typeof agent?.session?.id !== 'string') return
61
94
  if (config.audience !== 'all' && config.audience !== capabilityAudience(agent)) return
62
95
  let active = true
63
- const report = message => ctx.logger?.warn?.(`${name}: ${message}`)
96
+ const samples = { text: [], reasoning: [] }
97
+ const clearSamples = () => { samples.text.length = 0; samples.reasoning.length = 0 }
98
+ const observe = state => observations.set(agent, { state, observedAt: new Date().toISOString() })
99
+ const report = message => { observe('reported'); ctx.logger?.warn?.(`${name}: ${message}`) }
64
100
  const limit = capabilityAudience(agent) === 'subagent' ? config.subagentRecoveryLimit : config.mainRecoveryLimit
65
101
  const coordinator = recoveryCoordinator(ctx, agent, config, { ...options, recover(candidate) {
102
+ observe('candidate')
66
103
  if (!limit) return
67
104
  if (!options.recoveryState) { report('persistent recovery state unavailable; automatic recovery disabled'); return }
68
105
  try {
69
106
  const claimed = options.recoveryState.claim(agent.session.id, candidate.execution, limit)
70
107
  if (!claimed.claimed) { report(`recovery unavailable (${claimed.used}/${limit})`); return }
108
+ observe('claimed')
71
109
  void (async () => {
72
110
  try {
73
111
  await agent.whenIdle()
@@ -82,18 +120,21 @@ export function apply(ctx, raw, options = {}) {
82
120
  const text = '上一轮输出因异常重复或仅有推理而未完成。请停止重复,检查已完成的工作,并给出有效结果或明确说明阻塞原因。'
83
121
  + (maintenance ? `\n维护压缩已完成,实际来源事件:${maintenance.shadowedSeqs.join(',')}。` : '')
84
122
  agent.followup(pluginMessage('generation-recovery', text, '恢复异常生成'))
123
+ observe('enqueued')
85
124
  } catch (error) { report(String(error?.message ?? error)) }
86
125
  })()
87
126
  } catch (error) { report(String(error?.message ?? error)) }
88
127
  } }, () => active)
89
- const stop = ctx.effect(() => () => { active = false })
128
+ const stop = ctx.effect(() => () => { active = false; clearSamples() })
90
129
  const off = ctx.on('llm/stream', (options, next) => {
130
+ if (options.sessionId === agent.session.id && options.signal?.aborted) clearSamples()
91
131
  if (!active || options.signal?.aborted || options.purpose !== undefined || options.sessionId !== agent.session.id) return next()
92
132
  return (async function* () {
93
133
  const detectors = {
94
134
  text: repeatDetector(config.textRepeatChars, config.maxCycleChars),
95
135
  reasoning: repeatDetector(config.reasoningRepeatChars, config.maxCycleChars),
96
136
  }
137
+ const materials = { text: materialDetector(config, samples.text), reasoning: materialDetector(config, samples.reasoning) }
97
138
  const open = new Map()
98
139
  const closed = new Set()
99
140
  const finalized = new Set()
@@ -102,7 +143,7 @@ export function apply(ctx, raw, options = {}) {
102
143
  let reasoningChars = 0
103
144
  for await (const chunk of next()) {
104
145
  if (finalized.has(chunk.index)) continue
105
- if (!active || options.signal?.aborted) { yield chunk; continue }
146
+ if (!active || options.signal?.aborted) { clearSamples(); yield chunk; continue }
106
147
  if (chunk.type === 'tool-call-delta' || chunk.blockType === 'tool-call' || chunk.block?.type === 'tool-call') hasToolCall = true
107
148
  if (chunk.type === 'block-start') open.set(chunk.index, { type: chunk.blockType, text: '' })
108
149
  if (chunk.type === 'block-end') { open.delete(chunk.index); closed.add(chunk.index) }
@@ -114,6 +155,8 @@ export function apply(ctx, raw, options = {}) {
114
155
  if (kind === 'text' && chunk.text.trim()) visibleText = true
115
156
  let cutoff = safe ? detectors[kind](chunk.text) : undefined
116
157
  let cause = 'repeated-' + kind
158
+ const material = safe ? materials[kind].push(chunk.text) : undefined
159
+ if (material && (cutoff === undefined || material.offset < cutoff)) { cutoff = material.offset; cause = material.cause }
117
160
  if (safe && kind === 'reasoning' && !visibleText && config.silentReasoningChars > 0) {
118
161
  const chars = [...chunk.text]
119
162
  const remaining = config.silentReasoningChars - reasoningChars
@@ -147,7 +190,13 @@ export function apply(ctx, raw, options = {}) {
147
190
  yield { type: 'finish', reason: { kind: 'stop' } }
148
191
  return
149
192
  }
193
+ if (active && !options.signal?.aborted && !hasToolCall && config.crossCallRepeatChars > 0) {
194
+ for (const kind of ['text', 'reasoning']) {
195
+ const sample = materials[kind].sample()
196
+ if (sample) { samples[kind].push(sample); if (samples[kind].length > 2) samples[kind].shift() }
197
+ }
198
+ }
150
199
  })()
151
200
  })
152
- return () => { stop(); off(); coordinator.dispose() }
201
+ return () => { stop(); off(); coordinator.dispose(); observations.delete(agent) }
153
202
  }
package/engine/layers.mjs CHANGED
@@ -18,7 +18,7 @@ import { INJECTION_LAYERS, KNOWN_STRATEGIES, SESSION_VARIABLES_DISABLED } from '
18
18
  import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue, isReservedInterpolationName } from './interpolate.mjs'
19
19
  import { renderTemplate } from './template-frame.mjs'
20
20
  import { compareConfigSequence, compareTextPlacement } from './order.mjs'
21
- import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
21
+ import { ruleFrame, actionMatches, actionSoulReminder, recheckSoulReminder } from './conditions/evaluation.mjs'
22
22
  import { textRegistrationName, identityStampOf, actionStamp, injectedMessage, findActionDelivery } from './delivery.mjs'
23
23
 
24
24
  const name = 'prompt-config-engine'
@@ -303,10 +303,15 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, o
303
303
  for (const config of binding.group) {
304
304
  if (!active || context.signal?.aborted || !actionMatches(config, frame) || !matchesAgentScope(config, context.agent)) continue
305
305
  try {
306
+ const reminder = actionSoulReminder(config, frame)
306
307
  const text = config.layer === 'runtime-context' && (config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
307
308
  ? await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
308
309
  : configText(config, registry, warnOnce, context)
309
- if (text.length) blocks.push(text)
310
+ if (!recheckSoulReminder(config, frame, reminder)) continue
311
+ if (text.length) {
312
+ const stamp = reminder === undefined ? '' : `\n<!-- soul-reminder:${Buffer.from(JSON.stringify({ moduleId: config.sourceModuleId, sessionId: context.agent.session.id, pendingVersion: reminder.pendingVersion, epoch: reminder.epoch })).toString('base64')} -->`
313
+ blocks.push(text + stamp)
314
+ }
310
315
  } catch (error) { warnOnce(`${name}: rule text ${config.id} failed: ${String(error?.message ?? error)}`) }
311
316
  }
312
317
  if (active && !context.signal?.aborted) {
@@ -1,4 +1,4 @@
1
- export type RetrievalEvent = { readonly seq: number; readonly type: string }
1
+ export type RetrievalEvent = { readonly seq: number; readonly type: string; readonly time?: number }
2
2
  export type SessionReference = { kind: 'session'; moduleId: string; sessionId: string; seqs: number[]; revision: string; start: number; end: number }
3
3
  export const MAX_REFERENCE_CHARS: number
4
4
  export function sessionReferenceText(events: readonly RetrievalEvent[], seqs: readonly number[]): string
@@ -1,4 +1,4 @@
1
- import { bool, defineConfig, stringList } from './fields.mjs'
1
+ import { bool, defineConfig, stringList, text } from './fields.mjs'
2
2
  import { toolServiceName } from './service-names.mjs'
3
3
 
4
4
  export const name = 'retrieval'
@@ -7,6 +7,7 @@ export const configContract = defineConfig({
7
7
  enabled: bool({ default: false }),
8
8
  paths: stringList({ default: () => [], allowEmpty: true, dedupe: true }),
9
9
  sessions: stringList({ default: () => [], allowEmpty: true, dedupe: true }),
10
+ synonymsFile: text({ default: undefined, allowEmpty: false }),
10
11
  })
11
12
 
12
13
  export function apply(ctx, raw, options) {
@@ -16,6 +16,9 @@ export interface RuleManifestCheck {
16
16
  read(moduleId: string): boolean | undefined
17
17
  }
18
18
 
19
+ export interface SoulReminderFact { count: number; pendingVersion: string; epoch: number; due: boolean }
20
+ export interface RuleSoulReminder { read(moduleId: string, agent: unknown): SoulReminderFact | undefined }
21
+
19
22
  /** 判定上报载荷;仅覆盖插件依赖面(宿主按 `RuleOutcomeReport` 消费它)。 */
20
23
  export interface RuleOutcomeReport {
21
24
  moduleId?: string
@@ -37,6 +40,7 @@ export interface MountRuleSourcesOptions {
37
40
  variableStore?: RuleVariableStore
38
41
  /** 清单 checks 事实;缺省=manifest 条件恒不可用。 */
39
42
  manifestCheck?: RuleManifestCheck
43
+ soulReminder?: RuleSoulReminder
40
44
  /** 官方预设兜底;缺省=没有兜底(预设条件恒不命中)。 */
41
45
  standingMountFor?: StandingMountFor
42
46
  /** 注入的晋升相位实例对;缺省从协调器或自建池取。 */
@@ -82,7 +82,7 @@ function mountSources(ctx, sources, options = {}) {
82
82
  const bind = (item, on) => prepareAction(item.action, { plugin: options.plugin, warnOnce, promptConfigOptions: item.rule.promptConfigOptions, on })(ctx)
83
83
  try {
84
84
  for (const source of sources) {
85
- try { rebindConditions([source], { ctx, standingMountFor: options.standingMountFor, promotionPool: pool, variableStore: options.variableStore, manifestCheck: options.manifestCheck }) }
85
+ try { rebindConditions([source], { ctx, standingMountFor: options.standingMountFor, promotionPool: pool, variableStore: options.variableStore, manifestCheck: options.manifestCheck, soulReminder: options.soulReminder }) }
86
86
  catch (error) { throw sourceError(error, source.moduleId) }
87
87
  }
88
88
  const rules = sources.flatMap(source => source.rules.filter(rule => rule.enabled !== false))
@@ -115,7 +115,8 @@ function mountSources(ctx, sources, options = {}) {
115
115
  for (const item of point.items) {
116
116
  const { action, rule, moduleId } = item
117
117
  if (action.kind === 'inject-text') {
118
- const config = { ...action.compiledConfig, rule, sourceModuleId: moduleId, variableStore: options.variableStore, group: undefined, exclusive: false, enabled: true, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen, runtimeOrder: action.channelOrder, runtimeRuleIndex: item.ruleIndex }
118
+ const config = { ...action.compiledConfig, rule, sourceModuleId: moduleId, variableStore: options.variableStore,
119
+ group: undefined, exclusive: false, enabled: true, actionWhen: action.actionWhen, bypassRuleWhen: action.bypassRuleWhen, runtimeOrder: action.channelOrder, runtimeRuleIndex: item.ruleIndex }
119
120
  if (config.layer === 'pre-step') injections.get(moduleId).configs.push(config)
120
121
  else textConfigs.push(config)
121
122
  continue
@@ -18,6 +18,8 @@ import { record } from './record.mjs'
18
18
  /** 规则字段:只有 `if`/`then`/`else`;旧名 `when`/`do` 留在名单里,只为把 unknown fields 换成一句「已退役,改用 X」。 */
19
19
  const RULE_FIELDS = new Set(['id', 'name', 'enabled', 'layer', 'group', 'exclusive', 'if', 'then', 'else', 'when', 'do'])
20
20
  const nonempty = value => typeof value === 'string' && value.trim().length > 0
21
+ const hasSoulCondition = node => node !== null && typeof node === 'object'
22
+ && (Object.hasOwn(node, 'soul') || Object.values(node).some(hasSoulCondition))
21
23
 
22
24
  /** `prepend` 退役文案(动作层与 `inject-text.config` 共用):等价档只有「注册到最外层」一档,而 `inject-text` 连它也不接受。 */
23
25
  const PREPEND_RETIRED = '`prepend` 已取消 — 它是未文档化的注册后门(executor 直读 config.prepend),作用只有「注册到最外层」一档:非 inject-text 动作改用 `waterfallPosition: outermost`;`inject-text` 不接受该字段,没有等价写法'
@@ -232,7 +234,7 @@ export function compileRules(specs, options = {}) {
232
234
  // 条件的绑定**不在编译期**:`preset` 谓词的取法要挂载 scope 的 ctx(`conditions/preset.mjs`),
233
235
  // 准备期拿不到。这里显式剥掉挂载期选项——传了也不生效,免得调用方以为已经绑好。
234
236
  // variableStore / manifestCheck 与 ctx/standingMountFor 同理:绑定发生在挂载期,准备期传了也不生效。
235
- const { ctx: _ctx, standingMountFor: _standingMountFor, variableStore: _variableStore, manifestCheck: _manifestCheck, ...conditionOptions } = options
237
+ const { ctx: _ctx, standingMountFor: _standingMountFor, variableStore: _variableStore, manifestCheck: _manifestCheck, soulReminder: _soulReminder, ...conditionOptions } = options
236
238
  const ids = new Set()
237
239
  const pendingConfigs = []
238
240
  const moduleId = options.moduleId ?? options.sourceModuleId
@@ -294,6 +296,9 @@ export function compileRules(specs, options = {}) {
294
296
  const action = structuredClone(source)
295
297
  if (action.kind === 'inject-text') {
296
298
  action.config = injectionConfigSpec(spec, action, options)
299
+ if ((hasSoulCondition(ruleIf) || conditions.some(hasSoulCondition)) && !['pre-step', 'runtime-context'].includes(action.config.layer)) {
300
+ throw new TypeError('soul reminder conditions support only pre-step or runtime-context delivery')
301
+ }
297
302
  }
298
303
  const execution = ruleActionExecution(action)
299
304
  // 未知键检查必须在 normalizeActionGates **之后**:中性旧 audience/modelScope/promotion
@@ -413,6 +418,7 @@ function needsMountContext(node) {
413
418
  if (Object.hasOwn(node, 'variable')) return true
414
419
  // manifest 要读挂载 scope 的清单检查服务(准备期没有)。
415
420
  if (Object.hasOwn(node, 'manifest')) return true
421
+ if (Object.hasOwn(node, 'soul')) return true
416
422
  if (Object.hasOwn(node, 'phase') && node.phase?.subscribe !== false) return true
417
423
  if (node.anchor?.after !== undefined) return true
418
424
  return Object.values(node).some(needsMountContext)
@@ -10,6 +10,11 @@ export const configContract = defineConfig({
10
10
  enabled: bool({ default: false }), intervalMs: bounded(60000, 1000, 2147483647),
11
11
  wake: bool({ default: false }), audience: enumOf(['all', 'main', 'subagent'], { default: 'all' }),
12
12
  budgetBytes: bounded(12288, 1024, 65536),
13
+ pressureThresholds: passthrough((value, plugin, field) => {
14
+ if (value === undefined) return undefined
15
+ if (!Array.isArray(value) || value.length !== 3 || value.some((ratio, index) => typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio <= 0 || ratio >= 1 || index > 0 && ratio <= value[index - 1])) throw new TypeError(`${plugin}: ${field} must be three increasing ratios between 0 and 1`)
16
+ return [...value]
17
+ }),
13
18
  })
14
19
  export function apply(ctx, raw, options = {}) {
15
20
  const config = configContract.parse(raw, name)
package/lib/client.js CHANGED
@@ -744,6 +744,7 @@ window.__ModuleLoader__.load({
744
744
  manifest: "module_manifest",
745
745
  variables: "module_variables",
746
746
  soulLedger: "soul_ledger",
747
+ soulState: "soul_state",
747
748
  search: "module_search",
748
749
  open: "module_open"
749
750
  };
@@ -756,6 +757,7 @@ window.__ModuleLoader__.load({
756
757
  [MODULE_TOOL_NAMES.manifest]: "module-manifest-tools",
757
758
  [MODULE_TOOL_NAMES.variables]: "module-variable-tools",
758
759
  [MODULE_TOOL_NAMES.soulLedger]: "soul-ledger",
760
+ [MODULE_TOOL_NAMES.soulState]: "soul-ledger",
759
761
  [MODULE_TOOL_NAMES.search]: "retrieval",
760
762
  [MODULE_TOOL_NAMES.open]: "retrieval"
761
763
  };