dsh-advisor-plugin 0.2.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/cordis.patch.yml +6 -0
  4. package/lib/advisor-prompt.d.ts +28 -0
  5. package/lib/advisor-prompt.js +91 -0
  6. package/lib/client/AdvisorCard.d.ts +15 -0
  7. package/lib/client/AdvisorCard.js +60 -0
  8. package/lib/client/AdvisorToolRow.d.ts +40 -0
  9. package/lib/client/AdvisorToolRow.js +52 -0
  10. package/lib/client/PatrolNodeView.d.ts +18 -0
  11. package/lib/client/PatrolNodeView.js +57 -0
  12. package/lib/client/controller.d.ts +161 -0
  13. package/lib/client/controller.js +287 -0
  14. package/lib/client/index.d.ts +20 -0
  15. package/lib/client/index.js +841 -0
  16. package/lib/client/locales.d.ts +54 -0
  17. package/lib/client/locales.js +101 -0
  18. package/lib/client/patrol-chat.d.ts +78 -0
  19. package/lib/client/patrol-chat.js +54 -0
  20. package/lib/client/store.d.ts +11 -0
  21. package/lib/client/store.js +23 -0
  22. package/lib/command.d.ts +7 -0
  23. package/lib/command.js +52 -0
  24. package/lib/config.d.ts +37 -0
  25. package/lib/config.js +83 -0
  26. package/lib/emission-guard.d.ts +32 -0
  27. package/lib/emission-guard.js +74 -0
  28. package/lib/gating.d.ts +8 -0
  29. package/lib/gating.js +48 -0
  30. package/lib/history.d.ts +20 -0
  31. package/lib/history.js +95 -0
  32. package/lib/index.d.ts +25 -0
  33. package/lib/index.js +96 -0
  34. package/lib/llm-call.d.ts +69 -0
  35. package/lib/llm-call.js +483 -0
  36. package/lib/patrol-event.d.ts +88 -0
  37. package/lib/patrol-event.js +68 -0
  38. package/lib/patrol.d.ts +51 -0
  39. package/lib/patrol.js +202 -0
  40. package/lib/prompt-section.d.ts +7 -0
  41. package/lib/prompt-section.js +16 -0
  42. package/lib/settings.d.ts +18 -0
  43. package/lib/settings.js +50 -0
  44. package/lib/tool.d.ts +22 -0
  45. package/lib/tool.js +91 -0
  46. package/package.json +91 -0
  47. package/src/advisor-prompt.ts +102 -0
  48. package/src/client/AdvisorCard.tsx +253 -0
  49. package/src/client/AdvisorToolRow.tsx +83 -0
  50. package/src/client/PatrolNodeView.tsx +85 -0
  51. package/src/client/controller.ts +399 -0
  52. package/src/client/index.ts +100 -0
  53. package/src/client/locales.ts +105 -0
  54. package/src/client/patrol-chat.ts +109 -0
  55. package/src/client/store.ts +29 -0
  56. package/src/command.ts +61 -0
  57. package/src/config.ts +113 -0
  58. package/src/emission-guard.ts +76 -0
  59. package/src/gating.ts +54 -0
  60. package/src/history.ts +102 -0
  61. package/src/index.ts +106 -0
  62. package/src/llm-call.ts +552 -0
  63. package/src/patrol-event.ts +115 -0
  64. package/src/patrol.ts +229 -0
  65. package/src/prompt-section.ts +21 -0
  66. package/src/settings.ts +74 -0
  67. package/src/tool.ts +114 -0
package/src/patrol.ts ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * patrol —— 巡逻模式:把 advisor 从"被动求助"升级为"主动监控"。
3
+ *
4
+ * 工作方式:
5
+ * 1. agent/request 每步触发(复用现有 waterfall),按间隔步数 + 最小时间
6
+ * 间隔决定是否发起一次巡逻(不阻塞当前请求,异步执行);
7
+ * 2. 巡逻 = 把会话快照 + 巡逻问题发给审查模型,要求回
8
+ * ON-TRACK / CORRECTION: ... / STOP: ... 三选一裁决;
9
+ * 3. 裁决为纠偏/停止时,把干预文本经 `agent.inject()` 以 user 上下文排进
10
+ * 下一步请求——落在对话尾部(新近性最高,实测系统提示段在几百条工具
11
+ * 结果的会话里权重不足,执行模型"读到但不动");同时把裁决写入会话
12
+ * 日志(patrol-event.ts,log-only),消息流出 🧭 巡逻卡片(ON-TRACK
13
+ * 出低调灰卡,unclear 不出);
14
+ * 4. 侧调用(巡逻自身 + 显式 advisor() 工具)通过 llm-call 的
15
+ * 深度计数排除,避免把自己的请求当成执行请求。
16
+ *
17
+ * 成本提示:每次巡逻 = 整段会话计费一次审查模型(与显式 advisor()
18
+ * 相同)。patrolEnabled / patrolEverySteps 可在设置页调整或关闭。
19
+ */
20
+
21
+ import type { Context } from '@deepseek-ai/cordis'
22
+ import type { Message } from '@deepseek-ai/dsh-llm'
23
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
24
+ import type { Agent } from '@deepseek-ai/dsh-agent'
25
+ import { PATROL_NUDGE, PATROL_SYSTEM_PROMPT, buildInterventionText } from './advisor-prompt.js'
26
+ import type { Config, Selection } from './config.js'
27
+ import { isExecutorBlocked } from './config.js'
28
+ import { callReviewer, stripExecutorEcho } from './llm-call.js'
29
+ import { appendPatrolVerdict } from './patrol-event.js'
30
+ import { createAdviceDeduper, isContentFreeAdvice, normalizeAdvice } from './emission-guard.js'
31
+
32
+ /** 两次巡逻之间的最小墙钟间隔(防止快速工具链把审查模型打爆) */
33
+ export const MIN_PATROL_INTERVAL_MS = 90_000
34
+
35
+ /** 巡逻裁决(解析失败归为 unclear——不确定时宁可不打扰执行模型) */
36
+ export type PatrolVerdictKind = 'on-track' | 'correction' | 'stop' | 'unclear'
37
+
38
+ export interface PatrolVerdict {
39
+ kind: PatrolVerdictKind
40
+ detail: string
41
+ }
42
+
43
+ /** 是否该发起一次巡逻(纯函数,可单测) */
44
+ export function shouldPatrol(input: {
45
+ armed: boolean
46
+ blocked: boolean
47
+ step: number
48
+ everySteps: number
49
+ now: number
50
+ lastPatrolMs: number
51
+ minIntervalMs: number
52
+ inFlight: boolean
53
+ patrolEnabled: boolean
54
+ }): boolean {
55
+ if (!input.patrolEnabled || !input.armed || input.blocked || input.inFlight) return false
56
+ if (input.everySteps < 1) return false
57
+ if (input.step % input.everySteps !== 0) return false
58
+ return input.now - input.lastPatrolMs >= input.minIntervalMs
59
+ }
60
+
61
+ /** 解析审查模型的巡逻裁决(纯函数):只认首行前缀,其余整段作为细节 */
62
+ export function parsePatrolVerdict(text: string): PatrolVerdict {
63
+ const trimmed = text.trim()
64
+ if (trimmed === '') return { kind: 'unclear', detail: '' }
65
+ const firstLineEnd = trimmed.indexOf('\n')
66
+ const firstLine = (firstLineEnd === -1 ? trimmed : trimmed.slice(0, firstLineEnd)).trim()
67
+ const rest = firstLineEnd === -1 ? '' : trimmed.slice(firstLineEnd + 1).trim()
68
+ const upper = firstLine.toUpperCase()
69
+ if (upper.startsWith('ON-TRACK')) return { kind: 'on-track', detail: rest }
70
+ if (upper.startsWith('CORRECTION:') || upper.startsWith('CORRECTION')) {
71
+ const inline = firstLine.slice(firstLine.indexOf(':') + 1).trim()
72
+ return { kind: 'correction', detail: [inline, rest].filter(s => s !== '').join('\n') }
73
+ }
74
+ if (upper.startsWith('STOP:') || upper.startsWith('STOP')) {
75
+ const inline = firstLine.slice(firstLine.indexOf(':') + 1).trim()
76
+ return { kind: 'stop', detail: [inline, rest].filter(s => s !== '').join('\n') }
77
+ }
78
+ return { kind: 'unclear', detail: trimmed }
79
+ }
80
+
81
+ function createUserText(text: string): Message {
82
+ return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
83
+ }
84
+
85
+ export interface PatrolDeps {
86
+ getConfig: () => Config
87
+ getSelection: () => Selection | undefined
88
+ buildMessages: (agent: Agent) => Message[]
89
+ }
90
+
91
+ export function registerPatrol(ctx: Context, deps: PatrolDeps): () => void {
92
+ const stepCounts = new Map<Agent, number>()
93
+ /** 每 agent 的建议去重器(防噪闸之一:会话内精确去重) */
94
+ const dedupers = new Map<Agent, (normalized: string) => boolean>()
95
+ /** 每 agent 的纠偏冷却终点(步数):一次注入后 N 个请求内 concern 降级 */
96
+ const immuneUntilStep = new Map<Agent, number>()
97
+ let lastPatrolMs = 0
98
+ let inFlight = false
99
+
100
+ const runPatrol = async (agent: Agent) => {
101
+ const selection = deps.getSelection()
102
+ if (selection === undefined) return
103
+ inFlight = true
104
+ const startedAt = Date.now()
105
+ try {
106
+ // 快照末尾挂巡逻问题(与显式咨询共用消息组装,含工具清单前缀与尾部规则)
107
+ const messages = [...deps.buildMessages(agent), createUserText(PATROL_NUDGE)]
108
+ // 调查授权:审查模型可先在工作区内只读核实再裁决(cwd 缺省时不启用)
109
+ const investigate = deps.getConfig().investigate !== false ? agent.session.header.cwd : undefined
110
+ const outcome = await callReviewer(ctx, selection, PATROL_SYSTEM_PROMPT, messages, undefined, investigate, agent.session.id)
111
+ if (!outcome.ok) {
112
+ console.log(`[dsh-advisor][patrol] 巡逻调用失败(已跳过本轮):${outcome.errorMessage}`)
113
+ return
114
+ }
115
+ const verdict = parsePatrolVerdict(stripExecutorEcho(outcome.text, messages))
116
+ const elapsed = Math.round(Date.now() - startedAt)
117
+ if (verdict.kind === 'on-track') {
118
+ console.log(`[dsh-advisor][patrol] ON-TRACK(${elapsed}ms)——执行方向正常`)
119
+ // 低调灰卡:on-track 也写事件(用户拍板的完整视野);写失败不影响巡逻
120
+ appendPatrolVerdict(agent.session, {
121
+ verdict: 'on-track',
122
+ detail: verdict.detail,
123
+ step: stepCounts.get(agent) ?? 0,
124
+ reviewer: `${selection.provider}/${selection.model}`,
125
+ patrolMs: elapsed,
126
+ usage: outcome.usage,
127
+ })
128
+ return
129
+ }
130
+ if (verdict.kind === 'unclear') {
131
+ // 解析失败不出卡片(不是"检查通过"也不是"需要干预",只留日志)
132
+ console.log(`[dsh-advisor][patrol] 裁决不可解析(${elapsed}ms),不打扰执行模型。原文前 200 字:${outcome.text.slice(0, 200)}`)
133
+ return
134
+ }
135
+ console.log(`[dsh-advisor][patrol] ${verdict.kind === 'stop' ? 'STOP' : 'CORRECTION'}(${elapsed}ms):${verdict.detail.slice(0, 200)}`)
136
+ // 防噪闸 ①:内容空短语——只给结论不给理由的建议没有信息量,按不打扰处理
137
+ const normalized = normalizeAdvice(verdict.detail)
138
+ if (isContentFreeAdvice(normalized)) {
139
+ console.log(`[dsh-advisor][patrol] 裁决内容空(归一化后无实质信息),不打扰执行模型:${normalized.slice(0, 60)}`)
140
+ return
141
+ }
142
+ const deduper = dedupers.get(agent) ?? createAdviceDeduper()
143
+ dedupers.set(agent, deduper)
144
+ // 冷却降级:一次注入后 N 个请求内,correction 只出卡片不注入(stop 豁免)
145
+ const step = stepCounts.get(agent) ?? 0
146
+ const downgraded = verdict.kind === 'correction' && step < (immuneUntilStep.get(agent) ?? -1)
147
+ // 人类可观测性通道:裁决写入会话日志,消息流将出现 🧭 卡片(log-only)
148
+ appendPatrolVerdict(agent.session, {
149
+ verdict: verdict.kind,
150
+ detail: verdict.detail,
151
+ step,
152
+ reviewer: `${selection.provider}/${selection.model}`,
153
+ patrolMs: elapsed,
154
+ usage: outcome.usage,
155
+ ...(downgraded ? { downgraded: true } : {}),
156
+ })
157
+ console.log('[dsh-advisor][patrol] 裁决已写入会话事件')
158
+ // 防噪闸 ②:会话内精确去重——与已注入过的建议重复则丢弃(只记日志)
159
+ if (!downgraded && !deduper(normalized)) {
160
+ console.log('[dsh-advisor][patrol] 与此前已注入的纠偏重复,丢弃本次注入')
161
+ return
162
+ }
163
+ if (downgraded) {
164
+ console.log(`[dsh-advisor][patrol] 纠偏冷却期内(第 ${step} 步 < ${immuneUntilStep.get(agent)}),本次只出卡片不注入`)
165
+ return
166
+ }
167
+ // 模型面投递:agent.inject() 把干预排成下一步请求的尾部 user 上下文。
168
+ // 实测教训:系统提示动态段在几百条工具结果的会话里新近性不足,执行
169
+ // 模型"读到但不动";尾部上下文是文件变更通知/skill 内容同款官方通道,
170
+ // 位置就是模型注意力最集中的对话末尾,且 UI 会渲染成上下文行。
171
+ try {
172
+ const immuneTurns = deps.getConfig().patrolImmuneTurns ?? 3
173
+ if (verdict.kind === 'correction' && immuneTurns > 0) {
174
+ immuneUntilStep.set(agent, step + immuneTurns)
175
+ }
176
+ agent.inject(createUserMessage({
177
+ content: [{ type: 'text', text: buildInterventionText(verdict.kind, verdict.detail) }],
178
+ source: {
179
+ kind: 'plugin',
180
+ plugin: 'dsh-advisor',
181
+ form: 'notice',
182
+ summary: `${verdict.kind === 'stop' ? '巡逻叫停' : '巡逻纠偏'}:${verdict.detail.slice(0, 60)}`,
183
+ },
184
+ }))
185
+ console.log(`[dsh-advisor][patrol] 已将${verdict.kind === 'stop' ? '停止' : '纠偏'}干预排入下一步请求(尾部上下文)`)
186
+ } catch (error) {
187
+ // 注入失败不影响巡逻本身
188
+ console.warn('[dsh-advisor][patrol] 干预注入失败:', error)
189
+ }
190
+ } finally {
191
+ inFlight = false
192
+ }
193
+ }
194
+
195
+ const disposeRequest = ctx.on('agent/request', async (payload, next) => {
196
+ const config = await next()
197
+ try {
198
+ const agent = payload.agent
199
+ const step = (stepCounts.get(agent) ?? 0) + 1
200
+ stepCounts.set(agent, step)
201
+ const pluginConfig = deps.getConfig()
202
+ const blocked = isExecutorBlocked(pluginConfig, config.provider, config.model, config.reasoningEffort)
203
+ if (!shouldPatrol({
204
+ armed: deps.getSelection() !== undefined,
205
+ blocked,
206
+ step,
207
+ everySteps: pluginConfig.patrolEverySteps ?? 6,
208
+ now: Date.now(),
209
+ lastPatrolMs,
210
+ minIntervalMs: MIN_PATROL_INTERVAL_MS,
211
+ inFlight,
212
+ patrolEnabled: pluginConfig.patrolEnabled !== false,
213
+ })) return config
214
+ lastPatrolMs = Date.now()
215
+ void runPatrol(agent)
216
+ } catch (error) {
217
+ // 巡逻绝不破坏用户的 turn
218
+ console.error('[dsh-advisor][patrol] 调度失败:', error)
219
+ }
220
+ return config
221
+ })
222
+
223
+ return () => {
224
+ disposeRequest()
225
+ stepCounts.clear()
226
+ dedupers.clear()
227
+ immuneUntilStep.clear()
228
+ }
229
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * prompt-section —— 注入系统提示的升级守则段。
3
+ * 只在已选审查模型时注册——未武装的 advisor 零提示词成本。
4
+ */
5
+
6
+ import type { Context } from '@deepseek-ai/cordis'
7
+ import type {} from '@deepseek-ai/dsh-system-prompt'
8
+ import { DEFAULT_GUIDELINES } from './advisor-prompt.js'
9
+ import type { Config } from './config.js'
10
+
11
+ export function registerAdvisorSection(ctx: Context, config: Config): () => void {
12
+ const guidelines = config.guidelines === undefined || config.guidelines.length === 0
13
+ ? DEFAULT_GUIDELINES
14
+ : config.guidelines
15
+ const text = ['## Advisor escalation', '', ...guidelines.map(g => `- ${g}`)].join('\n')
16
+ return ctx.systemPrompt.section({
17
+ name: 'advisor-guidelines',
18
+ order: 550,
19
+ text,
20
+ })
21
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * settings —— 把 'advisor' 命名空间接入 settings 服务(用户层 =
3
+ * ~/.dsh/settings.yaml 的 advisor: 段;设置页卡片写的也是这一层)。
4
+ *
5
+ * 双路径兼容(rc.6 与更新版本宿主):
6
+ * - installSection(较新宿主):带 setSource/onChange 钩子的注册面
7
+ * - settings.register(rc.6):scope.get() 初读 + scope.watch() 活编辑
8
+ */
9
+
10
+ import type { Context } from '@deepseek-ai/cordis'
11
+ import type {} from '@deepseek-ai/dsh-settings'
12
+ import { Config as ConfigSchema, resolveSelection, type Config, type Selection } from './config.js'
13
+
14
+ export interface SettingsWiring {
15
+ /** 设置服务状态描述(进 /advisor 命令输出与日志) */
16
+ info: string
17
+ }
18
+
19
+ export function wireSettings(
20
+ ctx: Context,
21
+ state: { config: Config, selection: Selection | undefined },
22
+ onConfigChange: () => void,
23
+ ): SettingsWiring {
24
+ const settings = ctx.get('settings') as
25
+ | {
26
+ installSection?: (ctx: Context, ns: string, schema: unknown, entry: unknown, hooks: unknown) => void
27
+ register?: (ns: string, schema: unknown, options?: { base?: unknown }) => {
28
+ get: () => Config
29
+ watch: (cb: (next: Config) => void) => () => void
30
+ }
31
+ }
32
+ | undefined
33
+
34
+ if (settings === undefined) {
35
+ return { info: 'settings 服务不可用——advisor 配置固定为插件组合层' }
36
+ }
37
+
38
+ if (typeof settings.installSection === 'function') {
39
+ // 较新宿主:setSource 把"当前值来源"交给设置服务接管
40
+ let source: () => Config = () => state.config
41
+ settings.installSection(ctx, 'advisor', ConfigSchema, state.config, {
42
+ setSource: (next: () => Config) => {
43
+ source = next
44
+ },
45
+ onChange: () => {
46
+ state.config = source()
47
+ state.selection = resolveSelection(state.config)
48
+ onConfigChange()
49
+ },
50
+ })
51
+ return { info: 'settings.installSection 已接入(advisor 命名空间可在设置页编辑)' }
52
+ }
53
+
54
+ if (typeof settings.register === 'function') {
55
+ // rc.6 宿主:scope.get() 读合成值(schema 默认 → base → 用户层),watch 活编辑
56
+ try {
57
+ const scope = settings.register('advisor', ConfigSchema, { base: state.config })
58
+ const resolved = scope.get()
59
+ state.selection = resolveSelection(resolved)
60
+ state.config = resolved
61
+ onConfigChange()
62
+ scope.watch(next => {
63
+ state.config = next
64
+ state.selection = resolveSelection(next)
65
+ onConfigChange()
66
+ })
67
+ return { info: `settings.register 已接入(advisor 命名空间,用户层在 ~/.dsh/settings.yaml)` }
68
+ } catch (error) {
69
+ return { info: `settings.register 失败:${error instanceof Error ? error.message : String(error)}` }
70
+ }
71
+ }
72
+
73
+ return { info: 'settings 服务形态未知——advisor 配置固定为插件组合层' }
74
+ }
package/src/tool.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * tool —— 零参数 `advisor` 工具(defineTool)。
3
+ *
4
+ * execute() 解析当前选择 → 组装审查消息 → 侧调用 → 返回规范值;
5
+ * 每条失败路径同样返回值(渲染成可读文本),执行模型读到错误说明后
6
+ * 继续本轮——advisor 挂掉绝不炸掉执行模型的 turn。
7
+ */
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+ import { defineTool } from '@deepseek-ai/dsh-tools'
11
+ import type { ContentBlock, TextBlock } from '@deepseek-ai/dsh-llm'
12
+ import type { Agent } from '@deepseek-ai/dsh-agent'
13
+ import { ADVISOR_SYSTEM_PROMPT, ADVISOR_TOOL_DESCRIPTION, ADVISOR_TOOL_NAME, MSG_NO_AGENT_SCOPE, MSG_NO_MODEL } from './advisor-prompt.js'
14
+ import { selectionLabel, type Selection } from './config.js'
15
+ import { callReviewer, stripExecutorEcho } from './llm-call.js'
16
+
17
+ /** 工具结果信封:成功带指导文本,失败带可读错误(渲染层只取文本) */
18
+ export interface AdvisorValue {
19
+ ok: boolean
20
+ advisorModel: string
21
+ guidance?: string
22
+ effort?: string
23
+ finishKind?: string
24
+ errorMessage?: string
25
+ inputTokens?: number
26
+ outputTokens?: number
27
+ }
28
+
29
+ function renderText(value: AdvisorValue): string {
30
+ if (!value.ok) return value.errorMessage ?? 'Advisor call failed.'
31
+ return value.guidance ?? ''
32
+ }
33
+
34
+ /** 从工具结果内容块提取纯文本(presentResult 只拿得到渲染后的内容块) */
35
+ function textOfContent(content: ContentBlock[]): string {
36
+ return content
37
+ .filter((block): block is TextBlock => block.type === 'text')
38
+ .map(block => block.text)
39
+ .join('\n')
40
+ .trim()
41
+ }
42
+
43
+ export function createAdvisorTool(
44
+ ctx: Context,
45
+ getSelection: () => Selection | undefined,
46
+ buildMessages: (agent: Agent) => import('@deepseek-ai/dsh-llm').Message[],
47
+ getConfig: () => import('./config.js').Config,
48
+ ) {
49
+ return defineTool({
50
+ name: ADVISOR_TOOL_NAME,
51
+ description: ADVISOR_TOOL_DESCRIPTION,
52
+ parameters: {},
53
+ output: {
54
+ schema: {
55
+ type: 'object',
56
+ additionalProperties: false,
57
+ properties: {
58
+ ok: { type: 'boolean' },
59
+ advisorModel: { type: 'string' },
60
+ guidance: { type: 'string' },
61
+ effort: { type: 'string' },
62
+ finishKind: { type: 'string' },
63
+ errorMessage: { type: 'string' },
64
+ inputTokens: { type: 'number' },
65
+ outputTokens: { type: 'number' },
66
+ },
67
+ },
68
+ render: (_args, value): ContentBlock[] => [{ type: 'text', text: renderText(value as unknown as AdvisorValue) }],
69
+ },
70
+ // 专属卡片:调用中/结果两态都有 🧭 标识,结果卡片正文就是审查建议,
71
+ // 用户在消息列表展开即见"真的调了 advisor、建议是什么"
72
+ presentCall: () => ({ card: 'generic', title: '🧭 Advisor 咨询中…' }),
73
+ presentResult: (_args, result) => {
74
+ const text = textOfContent(result.content)
75
+ return {
76
+ card: 'generic',
77
+ title: result.isError ? '🧭 Advisor 调用失败' : '🧭 Advisor 建议',
78
+ ...(text === '' ? {} : { content: [{ type: 'text' as const, text }] }),
79
+ }
80
+ },
81
+ async execute(_args, exec) {
82
+ const selection = getSelection()
83
+ if (selection === undefined) {
84
+ return { ok: false, advisorModel: '(not configured)', errorMessage: MSG_NO_MODEL } satisfies AdvisorValue
85
+ }
86
+ if (exec.agent === undefined) {
87
+ return { ok: false, advisorModel: selectionLabel(selection), errorMessage: MSG_NO_AGENT_SCOPE } satisfies AdvisorValue
88
+ }
89
+ const messages = buildMessages(exec.agent)
90
+ // 调查授权与巡逻一致:审查模型可先只读核实再给建议
91
+ const investigate = getConfig().investigate !== false ? exec.agent.session.header.cwd : undefined
92
+ const outcome = await callReviewer(ctx, selection, ADVISOR_SYSTEM_PROMPT, messages, exec.signal, investigate, exec.agent.session.id)
93
+ if (!outcome.ok) {
94
+ return {
95
+ ok: false,
96
+ advisorModel: selectionLabel(selection),
97
+ ...(selection.effort === undefined ? {} : { effort: selection.effort }),
98
+ errorMessage: outcome.errorMessage,
99
+ } satisfies AdvisorValue
100
+ }
101
+ return {
102
+ ok: true,
103
+ advisorModel: selectionLabel(selection),
104
+ // 剥掉审查模型对执行模型"最后一句话"的回显(网关实测行为)
105
+ guidance: stripExecutorEcho(outcome.text, messages),
106
+ ...(selection.effort === undefined ? {} : { effort: selection.effort }),
107
+ finishKind: outcome.finishKind,
108
+ ...(outcome.usage === undefined
109
+ ? {}
110
+ : { inputTokens: outcome.usage.inputTokens, outputTokens: outcome.usage.outputTokens }),
111
+ } satisfies AdvisorValue
112
+ },
113
+ })
114
+ }