dsh-plugin-prompt-tool 0.3.1 → 0.4.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.
- package/README.md +7 -5
- package/lib/client.js +30 -4
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +4 -1
- package/lib/index.mjs +30 -59
- package/lib/preset-core.d.mts +5 -1
- package/lib/preset-core.mjs +130 -31
- package/package.json +20 -20
- package/plan.md +19 -3
- package/preset/agent.cordis.yml +443 -0
- package/preset/compaction-epoch.mjs +81 -0
- package/preset/context-gate.mjs +165 -0
- package/preset/custom-bash.mjs +243 -0
- package/preset/instruction-hint.mjs +217 -0
- package/preset/near-anchor.mjs +6 -19
- package/preset/prompt-injector.mjs +111 -126
- package/preset/router-first-turn.mjs +73 -70
- package/preset/router-guide.mjs +14 -20
- package/preset/shared.mjs +83 -0
- package/preset/skill-search.mjs +142 -0
- package/preset/tool-bootstrap.mjs +282 -0
|
@@ -1,127 +1,112 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompt-injector —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
29
|
-
export const name = 'prompt-injector'
|
|
30
|
-
|
|
31
|
-
/** No inject list: nothing to resolve, only event listeners. */
|
|
32
|
-
export const inject = []
|
|
33
|
-
|
|
34
|
-
export function apply(ctx, config) {
|
|
35
|
-
const promptText = typeof config.promptText === 'string' && config.promptText.length > 0
|
|
36
|
-
? config.promptText
|
|
37
|
-
: undefined
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!weOk && assistantRounds(agent) <= 1) return decision
|
|
113
|
-
injectedPrompt.add(session.id)
|
|
114
|
-
const message = {
|
|
115
|
-
id: newMessageId(),
|
|
116
|
-
role: 'user',
|
|
117
|
-
content: [{ type: 'text', text: promptText }],
|
|
118
|
-
source: {
|
|
119
|
-
kind: 'plugin',
|
|
120
|
-
plugin: 'prompt-injector',
|
|
121
|
-
form: 'notice',
|
|
122
|
-
summary: weOk ? 'prompt-tool 提示词(we 锚定确认后注入)' : 'prompt-tool 提示词(we 未确认,兜底注入)',
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
return { ...decision, messages: [message, ...decision.messages] }
|
|
126
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* prompt-injector — prompt-tool 的提示词注入层。
|
|
3
|
+
*
|
|
4
|
+
* 职责:会话晋升(首个 tool/call 或 assistant/message,与 promoteOn: either
|
|
5
|
+
* 同定义)后,turn1 reasoning 以 "we" 开头 → 下一轮注入 promptText 一次;
|
|
6
|
+
* we 未确认 → 最多再等一轮,仍无则强制注入(兜底,绝不卡死)。
|
|
7
|
+
*
|
|
8
|
+
* promptText 由宿主在生成 preset 时写入 config(默认等于 preset.md 原文)。
|
|
9
|
+
* AGENTS.md 的注入走 instruction-hint.mjs,不由本模块拼接。
|
|
10
|
+
*
|
|
11
|
+
* 实测依据:
|
|
12
|
+
* - 复杂任务 + 原版 2 工具 + maxTokens 1024 → turn1 reasoning "We need";
|
|
13
|
+
* - 简单任务模型直接调工具干活(无 we 首词),走兜底注入;
|
|
14
|
+
* - 锚定消息("Tools are not open yet")在 2 工具下不产生 we
|
|
15
|
+
* (模型回复 "The user says..." 叙述语气),已废弃。
|
|
16
|
+
*
|
|
17
|
+
* 状态全部从持久 session events 推导(resume/reload 安全):
|
|
18
|
+
* - promoted:events 含 tool/call 或 assistant/message;
|
|
19
|
+
* - 已注入:events 含 source.kind=plugin + source.plugin=prompt-injector 的
|
|
20
|
+
* 消息事件(注入消息本身就是持久事件),进程重启/插件重载后据此跳过;
|
|
21
|
+
* - 内存 Set/Map 只做进程内快路径 memo,不承载跨进程真相。
|
|
22
|
+
* 注入仅一次。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
26
|
+
import { newMessageId, PROMOTE_EVENTS } from './shared.mjs'
|
|
27
|
+
|
|
28
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
29
|
+
export const name = 'prompt-injector'
|
|
30
|
+
|
|
31
|
+
/** No inject list: nothing to resolve, only event listeners. */
|
|
32
|
+
export const inject = []
|
|
33
|
+
|
|
34
|
+
export function apply(ctx, config) {
|
|
35
|
+
const promptText = typeof config.promptText === 'string' && config.promptText.length > 0
|
|
36
|
+
? config.promptText
|
|
37
|
+
: undefined
|
|
38
|
+
|
|
39
|
+
/** Sessions whose promptText has been injected (append-only). */
|
|
40
|
+
const injectedPrompt = new Set()
|
|
41
|
+
/** Sessions whose first assistant reasoning has been scanned (memoized). */
|
|
42
|
+
const weScanned = new Map()
|
|
43
|
+
/** Epoch-aware promotion, same definition as tool-bootstrap/context-gate. */
|
|
44
|
+
const promotion = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
|
|
45
|
+
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 从持久 session events 推导「是否已经注入过 promptText」。
|
|
49
|
+
* 注入消息本身就是 user/message 持久事件(data 即消息本体),因此跨进程
|
|
50
|
+
* 重启 / 插件热重载后仍能识别;兼容 router-standard 观测到的
|
|
51
|
+
* `data.message` 嵌套形状。内存 Set 只做快路径 memo,真相在事件流。
|
|
52
|
+
*/
|
|
53
|
+
const hasInjectedPrompt = (session) =>
|
|
54
|
+
session.events.some((event) => {
|
|
55
|
+
const payload = event.data && typeof event.data.message === 'object' && event.data.message !== null
|
|
56
|
+
? event.data.message
|
|
57
|
+
: event.data
|
|
58
|
+
const source = payload?.source
|
|
59
|
+
return source?.kind === 'plugin' && source?.plugin === 'prompt-injector'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/** 第一个 assistant/message(锚定轮)的 reasoning 是否以 "we" 开头。 */
|
|
65
|
+
const isWeAnchored = (agent) => {
|
|
66
|
+
const session = agent.session
|
|
67
|
+
const cached = weScanned.get(session.id)
|
|
68
|
+
if (cached !== undefined) return cached
|
|
69
|
+
const first = session.events.find((event) => event.type === 'assistant/message')
|
|
70
|
+
// 首个 assistant 消息尚未落库时不要缓存 false,等待下一轮再查。
|
|
71
|
+
if (first === undefined) return false
|
|
72
|
+
const content = first.data?.message?.content ?? []
|
|
73
|
+
const reasoning = content.find((block) => block.type === 'reasoning')
|
|
74
|
+
const anchored = reasoning !== undefined && /^we\b/i.test(String(reasoning.text ?? '').trim())
|
|
75
|
+
weScanned.set(session.id, anchored)
|
|
76
|
+
return anchored
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 已落库的 assistant 回合数(we 未确认时的兜底轮计数)。 */
|
|
80
|
+
const assistantRounds = (agent) =>
|
|
81
|
+
agent.session.events.filter((event) => event.type === 'assistant/message').length
|
|
82
|
+
|
|
83
|
+
// 锚定确认后注入一次 promptText(we 确认立即注入;未确认最多再等一轮)。
|
|
84
|
+
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
85
|
+
const decision = await next()
|
|
86
|
+
if (decision.kind === 'reject') return decision
|
|
87
|
+
if (agent === undefined || promptText === undefined) return decision
|
|
88
|
+
if (!promotion.status(agent).promoted) return decision
|
|
89
|
+
const session = agent.session
|
|
90
|
+
if (session === undefined || injectedPrompt.has(session.id)) return decision
|
|
91
|
+
// 持久事件级幂等:进程重启/插件重载后,已注入消息仍在事件流里,据此跳过。
|
|
92
|
+
if (hasInjectedPrompt(session)) {
|
|
93
|
+
injectedPrompt.add(session.id)
|
|
94
|
+
return decision
|
|
95
|
+
}
|
|
96
|
+
const weOk = isWeAnchored(agent)
|
|
97
|
+
if (!weOk && assistantRounds(agent) <= 1) return decision
|
|
98
|
+
injectedPrompt.add(session.id)
|
|
99
|
+
const message = {
|
|
100
|
+
id: newMessageId('prompt-injector'),
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: [{ type: 'text', text: promptText }],
|
|
103
|
+
source: {
|
|
104
|
+
kind: 'plugin',
|
|
105
|
+
plugin: 'prompt-injector',
|
|
106
|
+
form: 'notice',
|
|
107
|
+
summary: weOk ? 'prompt-tool 提示词(we 锚定确认后注入)' : 'prompt-tool 提示词(we 未确认,兜底注入)',
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
return { ...decision, messages: [message, ...decision.messages] }
|
|
111
|
+
})
|
|
127
112
|
}
|
|
@@ -1,70 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* router-first-turn — prompt-tool 本地附加件(最优组合)。
|
|
3
|
-
*
|
|
4
|
-
* 依据 dsh-router-standard 的 router-bootstrap 组装逻辑:
|
|
5
|
-
* - 只替换 persona 段,保留计划模式段与其他第三方 section;
|
|
6
|
-
* - 首轮隐藏 mnemon:*
|
|
7
|
-
* - 按主会话模型自动选择 persona:
|
|
8
|
-
* Pro → 官方训练原句;
|
|
9
|
-
* Flash → dsh-router-standard 的 Flash 弱路由人设(build/fix 分类 +
|
|
10
|
-
* 回顾锚 + 收敛锚 + 反跑题锚 + 先深想再产出);
|
|
11
|
-
* - 子代理(委托深度 > 0)直接放行完整结果,保证 dsh-mnemon 等插件
|
|
12
|
-
* 通过工具白名单要求的 mnemon_* 工具首轮可见。
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
export const
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
/**
|
|
2
|
+
* router-first-turn — prompt-tool 本地附加件(最优组合)。
|
|
3
|
+
*
|
|
4
|
+
* 依据 dsh-router-standard 的 router-bootstrap 组装逻辑:
|
|
5
|
+
* - 只替换 persona 段,保留计划模式段与其他第三方 section;
|
|
6
|
+
* - 首轮隐藏 mnemon:* 自动注入段,晋升后恢复(contexts 由 context-gate 清空/恢复);
|
|
7
|
+
* - 按主会话模型自动选择 persona:
|
|
8
|
+
* Pro → 官方训练原句;
|
|
9
|
+
* Flash → dsh-router-standard 的 Flash 弱路由人设(build/fix 分类 +
|
|
10
|
+
* 回顾锚 + 收敛锚 + 反跑题锚 + 先深想再产出);
|
|
11
|
+
* - 子代理(委托深度 > 0)直接放行完整结果,保证 dsh-mnemon 等插件
|
|
12
|
+
* 通过工具白名单要求的 mnemon_* 工具首轮可见。
|
|
13
|
+
*
|
|
14
|
+
* 工具目录裁剪仍由 tool-bootstrap 负责(首轮 = 真实 Minimal 工具对;晋升后
|
|
15
|
+
* 恢复完整目录或切换 PTC),本模块不触碰 tools,避免两层过滤器冲突。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
19
|
+
import { isDelegated, isFlashModel, PROMOTE_EVENTS } from './shared.mjs'
|
|
20
|
+
|
|
21
|
+
/** Cordis 插件名,供 loader 诊断使用。 */
|
|
22
|
+
export const name = 'router-first-turn'
|
|
23
|
+
|
|
24
|
+
/** 无服务依赖,只监听 system-prompt 组装。 */
|
|
25
|
+
export const inject = []
|
|
26
|
+
|
|
27
|
+
/** 官方 RL 训练原句(保持与 Minimal 预设逐字节一致)。 */
|
|
28
|
+
const RL_PERSONA = 'You are a helpful software engineer assistant.'
|
|
29
|
+
|
|
30
|
+
/** dsh-router-standard 的 Flash 弱路由人设(build/fix 分类 + 三锚 + 深想)。 */
|
|
31
|
+
const FLASH_PERSONA = [
|
|
32
|
+
'You are a helpful assistant.',
|
|
33
|
+
'Before acting, decide the task type (build or fix) and adopt the matching style: build → hands-on production; fix → inspect-and-plan.',
|
|
34
|
+
'Before acting, briefly review what you have already done in this session and continue from where you left off; do not repeat completed steps. Do not run environment checks (echo, whoami, uname, node --version, date) or exhaustive grep/glob scans.',
|
|
35
|
+
'Think deeply first, then produce.',
|
|
36
|
+
].join('\n')
|
|
37
|
+
|
|
38
|
+
/** 是否为 persona 段:旧 persona 段由本模块替换。 */
|
|
39
|
+
function isPersonaSection(section) {
|
|
40
|
+
if (section === null || typeof section !== 'object') return false
|
|
41
|
+
const name = String(section.name ?? '')
|
|
42
|
+
return name === 'persona' || /persona/i.test(name)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 首轮需要隐藏的 mnemon 自动注入段(晋升后恢复)。 */
|
|
46
|
+
function isMnemonSection(section) {
|
|
47
|
+
if (section === null || typeof section !== 'object') return false
|
|
48
|
+
return String(section.name ?? '').startsWith('mnemon:')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function apply(ctx) {
|
|
52
|
+
const promotion = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
|
|
53
|
+
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
54
|
+
|
|
55
|
+
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
|
|
56
|
+
const assembled = await next()
|
|
57
|
+
const agent = context.agent
|
|
58
|
+
if (agent === undefined) return assembled
|
|
59
|
+
const session = agent.session
|
|
60
|
+
if (session === undefined) return assembled
|
|
61
|
+
|
|
62
|
+
// 子代理不裁剪任何 section/context:它与 dsh-mnemon 的工具白名单协作。
|
|
63
|
+
if (isDelegated(session)) return assembled
|
|
64
|
+
|
|
65
|
+
const persona = isFlashModel(agent.options?.model) ? FLASH_PERSONA : RL_PERSONA
|
|
66
|
+
|
|
67
|
+
const sections = assembled.sections ?? []
|
|
68
|
+
const promoted = promotion.status(agent).promoted
|
|
69
|
+
const kept = sections.filter((section) => !isPersonaSection(section) && (promoted || !isMnemonSection(section)))
|
|
70
|
+
// contexts 由 context-gate 统一清空/恢复,本模块只替换 persona 与隐藏 mnemon 段。
|
|
71
|
+
return { ...assembled, sections: [...kept, { name: 'router-persona', text: persona, order: 0 }] }
|
|
72
|
+
})
|
|
73
|
+
}
|
package/preset/router-guide.mjs
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* 子代理不注入;首轮不注入(首句锚定由 near-anchor 负责)。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
15
|
+
import { extractText, isDelegated, isFlashModel, newMessageId, PROMOTE_EVENTS } from './shared.mjs'
|
|
16
|
+
|
|
14
17
|
/** Cordis 插件名,供 loader 诊断使用。 */
|
|
15
18
|
export const name = 'router-guide'
|
|
16
19
|
|
|
17
|
-
/** 无服务依赖,只监听 pre-step
|
|
20
|
+
/** 无服务依赖,只监听 session/event 与 pre-step。 */
|
|
18
21
|
export const inject = []
|
|
19
22
|
|
|
20
23
|
/** 复杂任务判定:长度或架构关键词。 */
|
|
@@ -23,26 +26,19 @@ const COMPLEX_RE = /(架构|重构|全面|详细|设计|系统|优化|分析|arc
|
|
|
23
26
|
const GUIDE_WEAK = '\nRouter: classify this task (build or fix) now, then adopt the matching style — build: direct production; fix: inspect-first. Think deeply first, then commit and act.'
|
|
24
27
|
const GUIDE_DEEP = '\nRouter: classify this task (build or fix) now, then adopt the matching style — build: direct production; fix: inspect-first. Think deeply about the architecture, edge cases, and integration points. Do not spend reasoning on the environment or tooling. Produce when your information is complete. End each reasoning block with a decision or an information need.'
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: `router-guide-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
30
|
-
}
|
|
29
|
+
/** 宿主 UI 写入的默认编辑框文本;未改动时等价自动模式。 */
|
|
30
|
+
const DEFAULT_GUIDE_TEXT = ['简单任务自动引导:', GUIDE_WEAK.trim(), '', '复杂任务自动引导:', GUIDE_DEEP.trim()].join('\n')
|
|
31
|
+
|
|
31
32
|
|
|
32
|
-
function extractText(message) {
|
|
33
|
-
if (!message) return ''
|
|
34
|
-
const content = Array.isArray(message.content) ? message.content : []
|
|
35
|
-
return content.map((block) => (typeof block === 'string' ? block : (block?.text ?? ''))).join(' ').trim()
|
|
36
|
-
}
|
|
37
33
|
|
|
38
34
|
export function apply(ctx, config) {
|
|
39
35
|
const useCustom = config?.useCustom === true
|
|
40
36
|
const customText = typeof config?.text === 'string' ? config.text : ''
|
|
41
37
|
const enabled = config?.enabled !== false
|
|
42
|
-
// 与宿主写入的默认内容一致:打开自定义但未改动默认文本时,等价自动模式。
|
|
43
|
-
const DEFAULT_GUIDE_TEXT = ['简单任务自动引导:', GUIDE_WEAK.trim(), '', '复杂任务自动引导:', GUIDE_DEEP.trim()].join('\n')
|
|
44
38
|
const unchangedDefault = customText.trim() === DEFAULT_GUIDE_TEXT.trim()
|
|
45
|
-
|
|
39
|
+
|
|
40
|
+
const promotion = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
|
|
41
|
+
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
46
42
|
|
|
47
43
|
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
48
44
|
const decision = await next()
|
|
@@ -51,14 +47,12 @@ export function apply(ctx, config) {
|
|
|
51
47
|
const session = agent.session
|
|
52
48
|
if (session === undefined) return decision
|
|
53
49
|
// 子代理不注入。
|
|
54
|
-
if ((session
|
|
50
|
+
if (isDelegated(session)) return decision
|
|
55
51
|
// 自动模式只对 Flash 主会话注入;自定义开关打开后,Pro 与 Flash 一样注入。
|
|
56
|
-
const
|
|
57
|
-
const isFlash = typeof modelId === 'string' && /flash/i.test(modelId)
|
|
52
|
+
const isFlash = isFlashModel(agent.options?.model)
|
|
58
53
|
if (!useCustom && !isFlash) return decision
|
|
59
54
|
// 晋升后才注入(首轮由 near-anchor 负责)。
|
|
60
|
-
|
|
61
|
-
if (!promoted) return decision
|
|
55
|
+
if (!promotion.status(agent).promoted) return decision
|
|
62
56
|
|
|
63
57
|
const messages = Array.isArray(decision.messages) ? decision.messages : []
|
|
64
58
|
const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
|
|
@@ -75,7 +69,7 @@ export function apply(ctx, config) {
|
|
|
75
69
|
|
|
76
70
|
const nextMessages = [...messages]
|
|
77
71
|
nextMessages.splice(userIndex + 1, 0, {
|
|
78
|
-
id: newMessageId(),
|
|
72
|
+
id: newMessageId('router-guide'),
|
|
79
73
|
role: 'user',
|
|
80
74
|
content: [{ type: 'text', text: guide }],
|
|
81
75
|
source: { kind: 'router-guide', plugin: 'router-guide', form: 'notice', summary: 'router-guide 每轮近距离引导' },
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shared — small utilities shared by the prompt-tool preset scripts.
|
|
3
|
+
* Not a plugin row: no name/inject exports and no listeners.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Durable session event types that count as a promotion signal per mode. */
|
|
7
|
+
export const PROMOTE_EVENTS = {
|
|
8
|
+
'tool-call': ['tool/call'],
|
|
9
|
+
'assistant-message': ['assistant/message'],
|
|
10
|
+
either: ['tool/call', 'assistant/message'],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Parse the shared promoteOn vocabulary. */
|
|
14
|
+
export function parsePromoteOn(pluginName, value) {
|
|
15
|
+
if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
|
|
16
|
+
if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
|
|
17
|
+
throw new TypeError(`${pluginName}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Validate an optional boolean config flag with a default. */
|
|
21
|
+
export function booleanOption(pluginName, value, field, fallback) {
|
|
22
|
+
if (value === undefined) return fallback
|
|
23
|
+
if (typeof value !== 'boolean') {
|
|
24
|
+
throw new TypeError(`${pluginName}: ${field} must be a boolean`)
|
|
25
|
+
}
|
|
26
|
+
return value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validate the shared config envelope and unknown-key contract. */
|
|
30
|
+
export function validateConfig(pluginName, source, allowedKeys) {
|
|
31
|
+
const config = source === undefined ? {} : source
|
|
32
|
+
if (typeof config !== 'object' || config === null || Array.isArray(config)) {
|
|
33
|
+
throw new TypeError(`${pluginName}: config must be an object`)
|
|
34
|
+
}
|
|
35
|
+
const unknown = Object.keys(config).filter((key) => !allowedKeys.has(key))
|
|
36
|
+
if (unknown.length > 0) {
|
|
37
|
+
throw new TypeError(
|
|
38
|
+
`${pluginName}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...allowedKeys].sort().join(', ')}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One-shot logger guard shared by filters that degrade instead of throwing. */
|
|
45
|
+
export function createWarnOnce(ctx, pluginName) {
|
|
46
|
+
let warned = false
|
|
47
|
+
return (message) => {
|
|
48
|
+
if (warned) return
|
|
49
|
+
warned = true
|
|
50
|
+
try {
|
|
51
|
+
ctx.logger.warn(message)
|
|
52
|
+
} catch {
|
|
53
|
+
// Logger unavailable — the guard exists only to avoid spamming.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Random message id with a crypto.randomUUID fast path. */
|
|
59
|
+
export function newMessageId(prefix) {
|
|
60
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
61
|
+
? crypto.randomUUID()
|
|
62
|
+
: `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Extract plain text from a message or from a wrapped event data shape. */
|
|
66
|
+
export function extractText(message) {
|
|
67
|
+
if (!message) return ''
|
|
68
|
+
const payload = message && typeof message.message === 'object' && message.message !== null
|
|
69
|
+
? message.message
|
|
70
|
+
: message
|
|
71
|
+
const content = Array.isArray(payload.content) ? payload.content : []
|
|
72
|
+
return content.map((block) => (typeof block === 'string' ? block : (block?.text ?? ''))).join(' ').trim()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True for subagent sessions. */
|
|
76
|
+
export function isDelegated(session) {
|
|
77
|
+
return (session?.header?.delegationDepth ?? 0) > 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True when a model id looks like a Flash-family model. */
|
|
81
|
+
export function isFlashModel(modelId) {
|
|
82
|
+
return typeof modelId === 'string' && /flash/i.test(modelId)
|
|
83
|
+
}
|