dsh-plugin-prompt-tool 0.8.0 → 1.0.0

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 (75) hide show
  1. package/README.md +58 -50
  2. package/engine/character-tools.mjs +3 -6
  3. package/engine/compositions/library/agent-instructions.yml +1 -1
  4. package/engine/compositions/library/command-goal.yml +1 -1
  5. package/engine/compositions/library/compaction.yml +1 -1
  6. package/engine/compositions/library/delegation-ptc.yml +1 -1
  7. package/engine/compositions/library/delegation.yml +1 -1
  8. package/engine/compositions/library/persistent-shell.yml +1 -1
  9. package/engine/compositions/library/planning.yml +1 -1
  10. package/engine/compositions/library/skill-filesystem-cordis.yml +1 -1
  11. package/engine/compositions/library/skill-filesystem.yml +1 -1
  12. package/engine/compositions/library/tool-ask-user.yml +1 -1
  13. package/engine/compositions/library/tool-bash.yml +1 -1
  14. package/engine/compositions/library/tool-cordis.yml +1 -1
  15. package/engine/compositions/library/tool-fs-search.yml +1 -1
  16. package/engine/compositions/library/tool-fs.yml +1 -1
  17. package/engine/compositions/library/tool-goal.yml +1 -1
  18. package/engine/compositions/library/tool-jobs.yml +1 -1
  19. package/engine/compositions/library/tool-plugin-manager-disabled.yml +1 -1
  20. package/engine/compositions/library/tool-plugin-manager.yml +1 -1
  21. package/engine/compositions/library/tool-present.yml +1 -1
  22. package/engine/compositions/library/tool-presentation.yml +1 -1
  23. package/engine/compositions/library/tool-pwsh.yml +1 -1
  24. package/engine/compositions/library/tool-skill.yml +1 -1
  25. package/engine/compositions/library/tool-todo.yml +1 -1
  26. package/engine/compositions/library/tool-web.yml +1 -1
  27. package/engine/compositions/source/local/character-tools.yml +4 -4
  28. package/engine/compositions/source/local/prompt-config-engine.yml +1 -1
  29. package/engine/compositions/source/local/session-var-tools.yml +4 -4
  30. package/engine/compositions/source/local/world-book-tools.yml +4 -4
  31. package/engine/executor.mjs +39 -26
  32. package/engine/instruction-hint.mjs +6 -2
  33. package/engine/layers.mjs +38 -30
  34. package/engine/order.mjs +17 -0
  35. package/engine/schema.mjs +16 -6
  36. package/engine/session-var-tools.mjs +3 -6
  37. package/engine/st-render.mjs +8 -4
  38. package/engine/subagent-tool-policy.mjs +25 -10
  39. package/engine/world-book-tools.mjs +3 -6
  40. package/lib/client.js +3451 -3519
  41. package/lib/client.js.map +1 -1
  42. package/lib/index.d.mts +233 -272
  43. package/lib/index.mjs +1020 -1226
  44. package/lib/{preset-package-BnJqrU4R.mjs → module-package-Cm3fHHlp.mjs} +997 -594
  45. package/lib/{paths-CY5psBqE.d.mts → paths-BgL42sSM.d.mts} +191 -149
  46. package/lib/preset-transfer.d.mts +2 -2
  47. package/lib/preset-transfer.mjs +2 -2
  48. package/modules/ponytail/configs/0000-ponytail-rules.yml +102 -0
  49. package/modules/ponytail/configs/0010-ponytail-level-full.yml +19 -0
  50. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +19 -0
  51. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +19 -0
  52. package/modules/ponytail/module.yml +172 -0
  53. package/package.json +46 -80
  54. package/templates/20-system-section.yml +4 -1
  55. package/templates/70-subagent-maintenance.yml +1 -1
  56. package/templates/tools/50-delegate-worldbook.yml +1 -1
  57. package/preset/pt-cordis/preset.yml +0 -43
  58. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +0 -29
  59. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +0 -501
  60. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +0 -38
  61. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +0 -60
  62. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +0 -5
  63. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +0 -13
  64. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +0 -5
  65. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +0 -22
  66. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +0 -3
  67. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +0 -2
  68. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +0 -15
  69. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +0 -8
  70. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +0 -7
  71. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +0 -84
  72. package/preset/pt-custom/preset.yml +0 -15
  73. package/preset/pt-minimal/preset.yml +0 -27
  74. package/preset/pt-ptc/preset.yml +0 -45
  75. package/preset/pt-standard/preset.yml +0 -43
@@ -28,6 +28,7 @@ import { createEpochPromotion } from './compaction-epoch.mjs'
28
28
  import { wireLayers } from './layers.mjs'
29
29
  import { sessionVarsSnapshot } from './session-vars.mjs'
30
30
  import { selectStWorldBook } from './st-world-book.mjs'
31
+ import { compareConfigSequence } from './order.mjs'
31
32
 
32
33
  const name = 'prompt-config-engine'
33
34
 
@@ -253,28 +254,23 @@ export async function runPreStepBatch(options) {
253
254
  }
254
255
  }
255
256
 
256
- // order 升序(同值保持声明顺序):决定拼接顺序与同位置插入顺序。
257
- due.sort((a, b) => a.config.order - b.config.order)
257
+ // 模块来源按序号;独立调用仍按 order,同值保留稳定身份顺序。
258
+ due.sort((a, b) => compareConfigSequence(a.config, b.config))
258
259
 
259
- // merged 模式的多条提示词配置在首条配置的位置合并为一条消息;
260
- // 文本按内容块拼接,source 身份改用 merged:<position> 以保持持久幂等。
260
+ // 每个落位内只合并同来源的连续 merged 卡;其他落位不会穿插在本落位的输出中。
261
+ // source 仍用既有 merged:<position>,不因分段或排序改写持久去重身份。
261
262
  const orderedGroups = []
262
- const groupIndex = new Map()
263
+ const lastAtPosition = new Map()
263
264
  for (const entry of due) {
264
- const group = entry.config.mergeMode === 'merged'
265
- ? mergedIdentity(entry.config)
266
- : undefined
267
- if (group === undefined) {
268
- orderedGroups.push([entry])
269
- continue
265
+ const previous = lastAtPosition.get(entry.config.position)
266
+ const base = previous?.[0].config
267
+ if (entry.config.mergeMode === 'merged' && base?.mergeMode === 'merged'
268
+ && entry.config.sourceModuleId === base.sourceModuleId) previous.push(entry)
269
+ else {
270
+ const group = [entry]
271
+ orderedGroups.push(group)
272
+ lastAtPosition.set(entry.config.position, group)
270
273
  }
271
- let index = groupIndex.get(group)
272
- if (index === undefined) {
273
- index = orderedGroups.length
274
- groupIndex.set(group, index)
275
- orderedGroups.push([])
276
- }
277
- orderedGroups[index].push(entry)
278
274
  }
279
275
 
280
276
  const planned = []
@@ -345,15 +341,10 @@ export async function runPreStepBatch(options) {
345
341
  * @param options.officialInstructions 本 mount 的组合是否仍挂着官方指令加载行
346
342
  * (负责人冲突事实;协调器据此拒绝同时注入文件正文)。
347
343
  */
348
- export function applyPromptConfigs(ctx, configs, options = {}) {
344
+ function effectivePromptConfigs(configs) {
349
345
  const list = configs.filter((config) => config !== undefined && config !== null)
350
- const sourceId = typeof options.sourceId === 'string' && options.sourceId.length > 0
351
- ? options.sourceId
352
- : `preset:${String(list[0]?.id ?? 'unknown')}`
353
- // 空 mount 同样保留接管监听:装配事实与协调器迟到/HMR 不依赖是否有预设卡。
354
- // 互斥组:同一 group 且 exclusive=true 时,只保留排序后第一个 enabled 提示词配置。
355
346
  const claimedGroups = new Set()
356
- const effectiveList = list.filter((config) => {
347
+ return list.filter((config) => {
357
348
  if (config.enabled === false) return false
358
349
  if (config.group !== undefined && config.exclusive === true) {
359
350
  if (claimedGroups.has(config.group)) return false
@@ -361,6 +352,27 @@ export function applyPromptConfigs(ctx, configs, options = {}) {
361
352
  }
362
353
  return true
363
354
  })
355
+ }
356
+
357
+ /** 多模块仍各自拥有来源/互斥组;只有非 pre-step 层汇总接线,避免模块块状执行。 */
358
+ export function applyPromptConfigSources(ctx, sources, options = {}) {
359
+ const selected = sources.map(source => ({ ...source, configs: effectivePromptConfigs(source.configs) }))
360
+ const releases = selected.map(source => applyPromptConfigs(ctx, source.configs, { ...options, ...source, layers: false }))
361
+ const releaseLayers = wireLayers(ctx, selected.flatMap(source => source.configs)
362
+ .filter(config => config.layer !== 'pre-step'), createWarnOnce(ctx, name))
363
+ return () => {
364
+ for (const release of releases) release()
365
+ releaseLayers()
366
+ }
367
+ }
368
+
369
+ export function applyPromptConfigs(ctx, configs, options = {}) {
370
+ const list = configs.filter((config) => config !== undefined && config !== null)
371
+ const sourceId = typeof options.sourceId === 'string' && options.sourceId.length > 0
372
+ ? options.sourceId
373
+ : `preset:${String(list[0]?.id ?? 'unknown')}`
374
+ // 互斥组仍在本来源内筛选;空来源也保留协调器迟到/HMR 接管。
375
+ const effectiveList = effectivePromptConfigs(list)
364
376
  const injectedMemo = new Map()
365
377
  const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
366
378
  const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true })
@@ -401,7 +413,8 @@ export function applyPromptConfigs(ctx, configs, options = {}) {
401
413
 
402
414
  // 非 pre-step 提示词配置接入各自声明的官方层级通道(system-section /
403
415
  // runtime-context / agent-request / llm-stream / tool-pipeline)。
404
- const releaseLayers = wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
416
+ const releaseLayers = options.layers === false ? () => {}
417
+ : wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
405
418
 
406
419
  // 协调器已在(正常装配顺序:插件先加载、预设后挂载)时立即登记来源;
407
420
  // 迟到/消失由下面的监听器按「先撤旧再启新」处理。
@@ -19,7 +19,7 @@
19
19
  import { randomUUID } from 'node:crypto'
20
20
  import { readFileSync } from 'node:fs'
21
21
  import { createEpochPromotion } from './compaction-epoch.mjs'
22
- import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
22
+ import { booleanOption, createWarnOnce, getService, parsePromoteOn, validateConfig } from './shared.mjs'
23
23
 
24
24
  export const name = 'instruction-hint'
25
25
 
@@ -272,10 +272,14 @@ export function apply(ctx, config) {
272
272
  if (!promotion.status(agent).promoted) return decision
273
273
  if (!Array.isArray(decision.messages)) return decision
274
274
  if (agent?.session === undefined) return decision
275
+ const coordinator = getService(ctx, 'promptToolPreStep')
276
+ const messages = typeof coordinator?.filterInstructions === 'function'
277
+ ? coordinator.filterInstructions(agent, decision.messages)
278
+ : decision.messages
275
279
  // 1 换 1 的转换不能按长度判断(长度相同仍可能已转换),
276
280
  // instructionHintMessages 本身保留非目标消息,直接采用结果。
277
281
  const hintState = { instructionHinted: hasVisibleInstructionHint(agent.session) }
278
- return { ...decision, messages: instructionHintMessages(decision.messages, hintState, name) }
282
+ return { ...decision, messages: instructionHintMessages(messages, hintState, name) }
279
283
  } catch (error) {
280
284
  // 转换失败不阻断会话:保留原消息。
281
285
  warnOnce(`${name}: instruction hint conversion failed, keeping messages: ${String((error && error.message) || error)}`)
package/engine/layers.mjs CHANGED
@@ -18,6 +18,7 @@ import { KNOWN_STRATEGIES } from './schema.mjs'
18
18
  import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue } from './interpolate.mjs'
19
19
  import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
20
20
  import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
21
+ import { compareConfigSequence, compareTextPlacement } from './order.mjs'
21
22
 
22
23
  const name = 'prompt-config-engine'
23
24
  // 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
@@ -160,28 +161,31 @@ function officialChannelText(text, label, registry, warnOnce) {
160
161
  return result.text
161
162
  }
162
163
 
163
- /** 文本型层分组:merged 模式按位置分组,否则每条独立。 */
164
+ /** 只合并排序后同来源、同官方档位和位置的连续 merged 卡,不跨过穿插段。 */
164
165
  function textLayerGroups(configs) {
165
- const sorted = [...configs].sort((a, b) => a.order - b.order)
166
+ const sorted = [...configs].sort(compareTextPlacement)
166
167
  const groups = []
167
- const index = new Map()
168
168
  for (const config of sorted) {
169
- const key = config.mergeMode === 'merged' ? `merged:${config.position ?? ''}` : undefined
170
- if (key === undefined) {
171
- groups.push([config])
172
- continue
173
- }
174
- let at = index.get(key)
175
- if (at === undefined) {
176
- at = groups.length
177
- index.set(key, at)
178
- groups.push([])
179
- }
180
- groups[at].push(config)
169
+ const previous = groups.at(-1)
170
+ const base = previous?.[0]
171
+ if (config.mergeMode === 'merged' && base?.mergeMode === 'merged'
172
+ && config.sourceModuleId === base.sourceModuleId
173
+ && config.position === base.position && config.order === base.order) previous.push(config)
174
+ else groups.push([config])
181
175
  }
182
176
  return groups
183
177
  }
184
178
 
179
+ /** 官方同 order 按 name 排序:默认名编码安全整数序号,显式名仍由作者拥有。 */
180
+ function textRegistrationName(config, field) {
181
+ const explicit = config.params?.[field]
182
+ if (typeof explicit === 'string' && explicit.length > 0) return explicit
183
+ if (config.sourceModuleId === undefined) return config.id
184
+ // 非负安全整数最多 16 位,跨过四位文件前缀后仍保持数值顺序。
185
+ const sequence = String(config.sequence).padStart(16, '0')
186
+ return `prompt-tool:${sequence}:${config.sourceModuleId}:${config.id}`
187
+ }
188
+
185
189
  /** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
186
190
  function wireSystemSections(ctx, configs, registry, warnOnce) {
187
191
  const disposers = []
@@ -207,7 +211,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
207
211
  .join('\n\n')
208
212
  : groupText
209
213
  keepDisposer(ctx, systemPrompt.section({
210
- name: typeof base.params?.sectionName === 'string' && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
214
+ name: textRegistrationName(base, 'sectionName'),
211
215
  order: base.order,
212
216
  text,
213
217
  ...(base.params?.complete === true ? { complete: true } : {}),
@@ -244,7 +248,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
244
248
  const text = dynamic ? render : render()
245
249
  if (!dynamic && text.length === 0) continue
246
250
  keepDisposer(ctx, systemPrompt.context({
247
- name: typeof base.params?.contextName === 'string' && base.params.contextName.length > 0 ? base.params.contextName : base.id,
251
+ name: textRegistrationName(base, 'contextName'),
248
252
  order: base.order,
249
253
  text,
250
254
  }), `${name}: context ${base.id}`)
@@ -255,7 +259,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
255
259
  // 官方 provider 必须同步:先注册可渲染为空的私有变量占位,再在 waterfall 中填充。
256
260
  // 标记随官方排序/遮蔽进入本次 assembly,无会话缓存,也不依赖调用方 context 的对象身份。
257
261
  const placeholders = configs.filter(needsResolver)
258
- .sort((a, b) => a.order - b.order)
262
+ .sort(compareTextPlacement)
259
263
  if (placeholders.length === 0) return disposers
260
264
  const slotVariable = `pt_runtime_${newMessageId('context').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
261
265
  const slotText = `{{${slotVariable}}}`
@@ -265,7 +269,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
265
269
  keepDisposer(ctx, systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
266
270
  for (const config of placeholders) {
267
271
  try {
268
- const contextName = typeof config.params?.contextName === 'string' && config.params.contextName.length > 0 ? config.params.contextName : config.id
272
+ const contextName = textRegistrationName(config, 'contextName')
269
273
  keepDisposer(ctx, systemPrompt.context({
270
274
  name: contextName,
271
275
  order: config.order,
@@ -337,20 +341,20 @@ export function applyAgentRequestParams(params, base) {
337
341
 
338
342
  /** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
339
343
  function wireAgentRequests(ctx, configs, warnOnce) {
340
- const disposers = []
341
- for (const config of configs) {
342
- disposers.push(ctx.on('agent/request', async (payload, next) => {
343
- const base = await next()
344
+ if (configs.length === 0) return []
345
+ const ordered = configs.some(config => config.sequence !== undefined) ? configs : [...configs].reverse()
346
+ return [ctx.on('agent/request', async (payload, next) => {
347
+ let result = await next()
348
+ // 模块请求在 next 返回后按序合并;无文件来源的独立调用保持原回栈顺序。
349
+ for (const config of ordered) {
344
350
  try {
345
- if (!matchesAgentScope(config, payload?.agent)) return base
346
- return applyAgentRequestParams(config.params, base)
351
+ if (matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
347
352
  } catch (error) {
348
353
  warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
349
- return base
350
354
  }
351
- }))
352
- }
353
- return disposers
355
+ }
356
+ return result
357
+ })]
354
358
  }
355
359
 
356
360
  /** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
@@ -518,9 +522,12 @@ export function createTurnStopBudget() {
518
522
  function wireTurnStops(ctx, configs, warnOnce) {
519
523
  const disposers = []
520
524
  if (configs.length === 0) return disposers
521
- const budget = createTurnStopBudget()
525
+ const budgets = new Map()
522
526
 
523
527
  for (const config of configs) {
528
+ const source = config.sourceModuleId ?? ''
529
+ if (!budgets.has(source)) budgets.set(source, createTurnStopBudget())
530
+ const budget = budgets.get(source)
524
531
  disposers.push(ctx.on('agent/turn-stopping', ({ agent, turn } = {}) => {
525
532
  try {
526
533
  const session = agent?.session
@@ -646,6 +653,7 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
646
653
  * 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
647
654
  */
648
655
  export function wireLayers(ctx, configs, warnOnce) {
656
+ configs = [...configs].sort(compareConfigSequence)
649
657
  // 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
650
658
  const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce)
651
659
  const registered = [
@@ -0,0 +1,17 @@
1
+ /** 加载器私有元数据:不进入 YAML、公共配置或客户端载荷。 */
2
+ export const FILE_SEQUENCE = Symbol('prompt-tool:file-sequence')
3
+
4
+ const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0
5
+
6
+ /** 模块配置按持久序号执行;无物化来源的独立调用保留原 order 语义。 */
7
+ export function compareConfigSequence(a, b) {
8
+ const difference = (a.sequence ?? a.order ?? 0) - (b.sequence ?? b.order ?? 0)
9
+ if (difference !== 0) return difference
10
+ if (a.sourceModuleId === undefined && b.sourceModuleId === undefined) return 0
11
+ return compareText(a.sourceModuleId ?? '', b.sourceModuleId ?? '') || compareText(a.id, b.id)
12
+ }
13
+
14
+ /** 官方文本段的定位仍由 order 决定;序号只提供同位置的稳定顺序。 */
15
+ export function compareTextPlacement(a, b) {
16
+ return a.order - b.order || compareConfigSequence(a, b)
17
+ }
package/engine/schema.mjs CHANGED
@@ -9,6 +9,7 @@ import { sep } from 'node:path'
9
9
  import { parse as parseYaml } from './vendor/yaml/index.js'
10
10
  import { bindResolver } from './strategies.mjs'
11
11
  import { attachStRenderers } from './st-render.mjs'
12
+ import { FILE_SEQUENCE, compareConfigSequence } from './order.mjs'
12
13
  import { MATCH_LOGIC, createAnchorMatcher } from './anchor-match.mjs'
13
14
 
14
15
  const name = 'prompt-config-engine'
@@ -126,11 +127,14 @@ export function loadPromptConfigFiles(dirUrl) {
126
127
  const specs = []
127
128
  for (const fileName of promptConfigFileNames(entries)) {
128
129
  const raw = readFileSync(new URL(fileName, dirUrl), 'utf8')
129
- if (/\.json$/i.test(fileName)) {
130
- specs.push(JSON.parse(raw))
131
- } else {
132
- specs.push(parsePromptConfigYaml(raw))
130
+ const spec = /\.json$/i.test(fileName) ? JSON.parse(raw) : parsePromptConfigYaml(raw)
131
+ const prefix = /^(\d+)-/.exec(fileName)
132
+ if (prefix !== null && spec !== null && typeof spec === 'object' && !Array.isArray(spec)) {
133
+ const sequence = Number(prefix[1])
134
+ if (!Number.isSafeInteger(sequence)) throw new TypeError(`${name}: unsafe config sequence in ${fileName}`)
135
+ Object.defineProperty(spec, FILE_SEQUENCE, { value: sequence })
133
136
  }
137
+ specs.push(spec)
134
138
  }
135
139
  for (const spec of specs) {
136
140
  if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) continue
@@ -632,17 +636,23 @@ export function createPromptConfigs(specs, options = {}) {
632
636
  templatePatch,
633
637
  params,
634
638
  }
639
+ const sequence = (Object.hasOwn(options.configOrder ?? {}, spec.id) ? options.configOrder[spec.id] : undefined) ?? spec[FILE_SEQUENCE]
640
+ ?? (typeof options.sourceModuleId === 'string' ? index * 10 : undefined)
641
+ if (sequence !== undefined) config.sequence = sequence
642
+ if (typeof options.sourceModuleId === 'string') config.sourceModuleId = options.sourceModuleId
635
643
  // 条件判定的匹配器在挂载期编译一次,执行侧(condition.mjs)直接复用:
636
644
  // normalizeMatch 已用同一份参数试编译过,这里是同源的第二句(失败会抛出)。
637
645
  config.matchScan = match === undefined ? undefined : createAnchorMatcher(match).scan
638
646
  config.resolve = bindResolver(config, options.strategyDir)
639
647
  return config
640
648
  })
641
- // 排序契约:anchor 提示词配置保持模块文件相对顺序(固定锚点),ordered 提示词配置按 order
642
- // 稳定升序排在其后。默认 order=0 时等价于文件顺序。
649
+ // 模块/物化来源按持久序号;没有文件来源的独立数组继续采用 anchor + order 契约。
643
650
  return attachStRenderers(configs
644
651
  .map((config, fileOrder) => ({ config, fileOrder }))
645
652
  .sort((a, b) => {
653
+ if (a.config.sequence !== undefined || b.config.sequence !== undefined) {
654
+ return compareConfigSequence(a.config, b.config) || a.fileOrder - b.fileOrder
655
+ }
646
656
  const aAnchor = a.config.configKind === 'anchor' ? 0 : 1
647
657
  const bAnchor = b.config.configKind === 'anchor' ? 0 : 1
648
658
  if (aAnchor !== bAnchor) return aAnchor - bAnchor
@@ -2,10 +2,7 @@
2
2
  export const name = "session-var-tools"
3
3
 
4
4
  export function apply(ctx) {
5
- const service = ctx.get("pt-session-var-tools")
6
- if (service === null || service === undefined || typeof service.mount !== "function") {
7
- ctx.logger?.warn(name + ": service unavailable; session-var tools not mounted for this preset")
8
- return
9
- }
10
- ctx.effect(() => service.mount(ctx), name + ": mount")
5
+ ctx.inject(["pt-session-var-tools"], (scopeCtx) => {
6
+ scopeCtx.effect(() => scopeCtx.get("pt-session-var-tools").mount(scopeCtx), name + ": mount")
7
+ })
11
8
  }
@@ -5,6 +5,7 @@ import { sessionVarsSnapshot } from './session-vars.mjs'
5
5
  import { sessionEvents, isDelegated, matchesModel } from './shared.mjs'
6
6
  import { stripUnresolvedRefs } from './interpolate.mjs'
7
7
  import { isSuccessfulCompactionEnd } from './compaction-epoch.mjs'
8
+ import { compareConfigSequence } from './order.mjs'
8
9
 
9
10
  function visible(config, agent) {
10
11
  if (config.enabled === false) return false
@@ -92,10 +93,13 @@ export function attachStRenderers(configs) {
92
93
  frame.evaluate = evaluate
93
94
  if (session) sessions.set(session, frame)
94
95
  }
95
- // 同一帧允许后到的 pre-step 资格补入;已求值模板不重放副作用或随机宏。
96
- for (const config of templates.filter(config => approved(config)
97
- && config.strategy === 'static' && config.dedupe === 'none' && !frame.text.has(config))
98
- .sort((a, b) => a.order - b.order)) frame.evaluate(config)
96
+ // 模块 pre-step 已跨来源按序调度,不能预跑本模块后面的卡而越过另一模块。
97
+ // 其它入口沿用原预求值语义;同帧已求值模板不重放副作用或随机宏。
98
+ if (target.sequence === undefined || eligible === undefined) {
99
+ for (const config of templates.filter(config => approved(config)
100
+ && config.strategy === 'static' && config.dedupe === 'none' && !frame.text.has(config))
101
+ .sort(compareConfigSequence)) frame.evaluate(config)
102
+ }
99
103
  if (token && typeof token === 'object') tokens.set(token, frame)
100
104
  if (!frame.text.has(target)) frame.evaluate(target)
101
105
  return frame.text.get(target) ?? ''
@@ -291,22 +291,30 @@ export function apply(ctx, config) {
291
291
  }
292
292
  const compositionScope = scopeOf(ctx)
293
293
  if (compositionScope === undefined) throw new Error(`${name}: requires a preset scope`)
294
- const installs = new WeakMap()
294
+ const installs = new Map()
295
+ const installing = new WeakSet()
296
+ let active = true
295
297
  const belongsToComposition = (agent) => scopeChainOf(scopeOf(agent.ctx)).includes(compositionScope)
296
298
  const install = (agent) => {
297
- if (installs.has(agent) || !belongsToComposition(agent)) return
299
+ if (!active || installing.has(agent) || installs.has(agent) || !belongsToComposition(agent)) return
298
300
  // 注册走 disposer 契约。这里保留 ctx.effect 而不是 shared.keepDisposer,两条依据:
299
301
  // (1) 两个 shadow 必须整组注册:任一个失败时由 Cordis 回滚已注册的那个
300
302
  // (keepDisposer 无回滚语义,第二个失败会留下第一个的残留注册);
301
303
  // (2) remove(agent) 需要按 agent 定向撤销,而 keepDisposer 返回 void,拿不到句柄。
302
- const dispose = agent.ctx.effect(() => {
303
- const disposers = [
304
- agent.ctx.tools.register(createShadowTool(ctx, agent.ctx.tools, compositionScope, compiled, 'spawn', source)),
305
- agent.ctx.tools.register(createShadowTool(ctx, agent.ctx.tools, compositionScope, compiled, 'fork', source)),
306
- ]
307
- return () => { for (const item of disposers) item() }
308
- }, `${name}: ${agent.id} shadow`)
309
- installs.set(agent, dispose)
304
+ // register/unregister 同步发 tools/change;登记完成前的重入不能再次注册同名工具。
305
+ installing.add(agent)
306
+ try {
307
+ const dispose = agent.ctx.effect(() => {
308
+ const disposers = [
309
+ agent.ctx.tools.register(createShadowTool(ctx, agent.ctx.tools, compositionScope, compiled, 'spawn', source)),
310
+ agent.ctx.tools.register(createShadowTool(ctx, agent.ctx.tools, compositionScope, compiled, 'fork', source)),
311
+ ]
312
+ return () => { for (const item of disposers) item() }
313
+ }, `${name}: ${agent.id} shadow`)
314
+ installs.set(agent, dispose)
315
+ } finally {
316
+ installing.delete(agent)
317
+ }
310
318
  }
311
319
  const remove = (agent) => {
312
320
  const dispose = installs.get(agent)
@@ -323,5 +331,12 @@ export function apply(ctx, config) {
323
331
  }
324
332
  }
325
333
  ctx.on('tools/change', reconcile)
334
+ ctx.effect(() => () => {
335
+ // Agent 可以继续存活:模块先停接线,再撤回其持有的 Agent scope 贡献。
336
+ active = false
337
+ const disposers = [...installs.values()]
338
+ installs.clear()
339
+ return Promise.all(disposers.map(dispose => dispose())).then(() => undefined)
340
+ }, `${name}: shadows`)
326
341
  reconcile()
327
342
  }
@@ -2,10 +2,7 @@
2
2
  export const name = "world-book-tools"
3
3
 
4
4
  export function apply(ctx) {
5
- const service = ctx.get("pt-world-book-tools")
6
- if (service === null || service === undefined || typeof service.mount !== "function") {
7
- ctx.logger?.warn(name + ": service unavailable; world-book tools not mounted for this preset")
8
- return
9
- }
10
- ctx.effect(() => service.mount(ctx), name + ": mount")
5
+ ctx.inject(["pt-world-book-tools"], (scopeCtx) => {
6
+ scopeCtx.effect(() => scopeCtx.get("pt-world-book-tools").mount(scopeCtx), name + ": mount")
7
+ })
11
8
  }