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.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/cordis.patch.yml +6 -0
- package/lib/advisor-prompt.d.ts +28 -0
- package/lib/advisor-prompt.js +91 -0
- package/lib/client/AdvisorCard.d.ts +15 -0
- package/lib/client/AdvisorCard.js +60 -0
- package/lib/client/AdvisorToolRow.d.ts +40 -0
- package/lib/client/AdvisorToolRow.js +52 -0
- package/lib/client/PatrolNodeView.d.ts +18 -0
- package/lib/client/PatrolNodeView.js +57 -0
- package/lib/client/controller.d.ts +161 -0
- package/lib/client/controller.js +287 -0
- package/lib/client/index.d.ts +20 -0
- package/lib/client/index.js +841 -0
- package/lib/client/locales.d.ts +54 -0
- package/lib/client/locales.js +101 -0
- package/lib/client/patrol-chat.d.ts +78 -0
- package/lib/client/patrol-chat.js +54 -0
- package/lib/client/store.d.ts +11 -0
- package/lib/client/store.js +23 -0
- package/lib/command.d.ts +7 -0
- package/lib/command.js +52 -0
- package/lib/config.d.ts +37 -0
- package/lib/config.js +83 -0
- package/lib/emission-guard.d.ts +32 -0
- package/lib/emission-guard.js +74 -0
- package/lib/gating.d.ts +8 -0
- package/lib/gating.js +48 -0
- package/lib/history.d.ts +20 -0
- package/lib/history.js +95 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +96 -0
- package/lib/llm-call.d.ts +69 -0
- package/lib/llm-call.js +483 -0
- package/lib/patrol-event.d.ts +88 -0
- package/lib/patrol-event.js +68 -0
- package/lib/patrol.d.ts +51 -0
- package/lib/patrol.js +202 -0
- package/lib/prompt-section.d.ts +7 -0
- package/lib/prompt-section.js +16 -0
- package/lib/settings.d.ts +18 -0
- package/lib/settings.js +50 -0
- package/lib/tool.d.ts +22 -0
- package/lib/tool.js +91 -0
- package/package.json +91 -0
- package/src/advisor-prompt.ts +102 -0
- package/src/client/AdvisorCard.tsx +253 -0
- package/src/client/AdvisorToolRow.tsx +83 -0
- package/src/client/PatrolNodeView.tsx +85 -0
- package/src/client/controller.ts +399 -0
- package/src/client/index.ts +100 -0
- package/src/client/locales.ts +105 -0
- package/src/client/patrol-chat.ts +109 -0
- package/src/client/store.ts +29 -0
- package/src/command.ts +61 -0
- package/src/config.ts +113 -0
- package/src/emission-guard.ts +76 -0
- package/src/gating.ts +54 -0
- package/src/history.ts +102 -0
- package/src/index.ts +106 -0
- package/src/llm-call.ts +552 -0
- package/src/patrol-event.ts +115 -0
- package/src/patrol.ts +229 -0
- package/src/prompt-section.ts +21 -0
- package/src/settings.ts +74 -0
- package/src/tool.ts +114 -0
package/lib/patrol.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import type { Message } from '@deepseek-ai/dsh-llm';
|
|
22
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
23
|
+
import type { Config, Selection } from './config.js';
|
|
24
|
+
/** 两次巡逻之间的最小墙钟间隔(防止快速工具链把审查模型打爆) */
|
|
25
|
+
export declare const MIN_PATROL_INTERVAL_MS = 90000;
|
|
26
|
+
/** 巡逻裁决(解析失败归为 unclear——不确定时宁可不打扰执行模型) */
|
|
27
|
+
export type PatrolVerdictKind = 'on-track' | 'correction' | 'stop' | 'unclear';
|
|
28
|
+
export interface PatrolVerdict {
|
|
29
|
+
kind: PatrolVerdictKind;
|
|
30
|
+
detail: string;
|
|
31
|
+
}
|
|
32
|
+
/** 是否该发起一次巡逻(纯函数,可单测) */
|
|
33
|
+
export declare function shouldPatrol(input: {
|
|
34
|
+
armed: boolean;
|
|
35
|
+
blocked: boolean;
|
|
36
|
+
step: number;
|
|
37
|
+
everySteps: number;
|
|
38
|
+
now: number;
|
|
39
|
+
lastPatrolMs: number;
|
|
40
|
+
minIntervalMs: number;
|
|
41
|
+
inFlight: boolean;
|
|
42
|
+
patrolEnabled: boolean;
|
|
43
|
+
}): boolean;
|
|
44
|
+
/** 解析审查模型的巡逻裁决(纯函数):只认首行前缀,其余整段作为细节 */
|
|
45
|
+
export declare function parsePatrolVerdict(text: string): PatrolVerdict;
|
|
46
|
+
export interface PatrolDeps {
|
|
47
|
+
getConfig: () => Config;
|
|
48
|
+
getSelection: () => Selection | undefined;
|
|
49
|
+
buildMessages: (agent: Agent) => Message[];
|
|
50
|
+
}
|
|
51
|
+
export declare function registerPatrol(ctx: Context, deps: PatrolDeps): () => void;
|
package/lib/patrol.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
21
|
+
import { PATROL_NUDGE, PATROL_SYSTEM_PROMPT, buildInterventionText } from './advisor-prompt.js';
|
|
22
|
+
import { isExecutorBlocked } from './config.js';
|
|
23
|
+
import { callReviewer, stripExecutorEcho } from './llm-call.js';
|
|
24
|
+
import { appendPatrolVerdict } from './patrol-event.js';
|
|
25
|
+
import { createAdviceDeduper, isContentFreeAdvice, normalizeAdvice } from './emission-guard.js';
|
|
26
|
+
/** 两次巡逻之间的最小墙钟间隔(防止快速工具链把审查模型打爆) */
|
|
27
|
+
export const MIN_PATROL_INTERVAL_MS = 90_000;
|
|
28
|
+
/** 是否该发起一次巡逻(纯函数,可单测) */
|
|
29
|
+
export function shouldPatrol(input) {
|
|
30
|
+
if (!input.patrolEnabled || !input.armed || input.blocked || input.inFlight)
|
|
31
|
+
return false;
|
|
32
|
+
if (input.everySteps < 1)
|
|
33
|
+
return false;
|
|
34
|
+
if (input.step % input.everySteps !== 0)
|
|
35
|
+
return false;
|
|
36
|
+
return input.now - input.lastPatrolMs >= input.minIntervalMs;
|
|
37
|
+
}
|
|
38
|
+
/** 解析审查模型的巡逻裁决(纯函数):只认首行前缀,其余整段作为细节 */
|
|
39
|
+
export function parsePatrolVerdict(text) {
|
|
40
|
+
const trimmed = text.trim();
|
|
41
|
+
if (trimmed === '')
|
|
42
|
+
return { kind: 'unclear', detail: '' };
|
|
43
|
+
const firstLineEnd = trimmed.indexOf('\n');
|
|
44
|
+
const firstLine = (firstLineEnd === -1 ? trimmed : trimmed.slice(0, firstLineEnd)).trim();
|
|
45
|
+
const rest = firstLineEnd === -1 ? '' : trimmed.slice(firstLineEnd + 1).trim();
|
|
46
|
+
const upper = firstLine.toUpperCase();
|
|
47
|
+
if (upper.startsWith('ON-TRACK'))
|
|
48
|
+
return { kind: 'on-track', detail: rest };
|
|
49
|
+
if (upper.startsWith('CORRECTION:') || upper.startsWith('CORRECTION')) {
|
|
50
|
+
const inline = firstLine.slice(firstLine.indexOf(':') + 1).trim();
|
|
51
|
+
return { kind: 'correction', detail: [inline, rest].filter(s => s !== '').join('\n') };
|
|
52
|
+
}
|
|
53
|
+
if (upper.startsWith('STOP:') || upper.startsWith('STOP')) {
|
|
54
|
+
const inline = firstLine.slice(firstLine.indexOf(':') + 1).trim();
|
|
55
|
+
return { kind: 'stop', detail: [inline, rest].filter(s => s !== '').join('\n') };
|
|
56
|
+
}
|
|
57
|
+
return { kind: 'unclear', detail: trimmed };
|
|
58
|
+
}
|
|
59
|
+
function createUserText(text) {
|
|
60
|
+
return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } });
|
|
61
|
+
}
|
|
62
|
+
export function registerPatrol(ctx, deps) {
|
|
63
|
+
const stepCounts = new Map();
|
|
64
|
+
/** 每 agent 的建议去重器(防噪闸之一:会话内精确去重) */
|
|
65
|
+
const dedupers = new Map();
|
|
66
|
+
/** 每 agent 的纠偏冷却终点(步数):一次注入后 N 个请求内 concern 降级 */
|
|
67
|
+
const immuneUntilStep = new Map();
|
|
68
|
+
let lastPatrolMs = 0;
|
|
69
|
+
let inFlight = false;
|
|
70
|
+
const runPatrol = async (agent) => {
|
|
71
|
+
const selection = deps.getSelection();
|
|
72
|
+
if (selection === undefined)
|
|
73
|
+
return;
|
|
74
|
+
inFlight = true;
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
try {
|
|
77
|
+
// 快照末尾挂巡逻问题(与显式咨询共用消息组装,含工具清单前缀与尾部规则)
|
|
78
|
+
const messages = [...deps.buildMessages(agent), createUserText(PATROL_NUDGE)];
|
|
79
|
+
// 调查授权:审查模型可先在工作区内只读核实再裁决(cwd 缺省时不启用)
|
|
80
|
+
const investigate = deps.getConfig().investigate !== false ? agent.session.header.cwd : undefined;
|
|
81
|
+
const outcome = await callReviewer(ctx, selection, PATROL_SYSTEM_PROMPT, messages, undefined, investigate, agent.session.id);
|
|
82
|
+
if (!outcome.ok) {
|
|
83
|
+
console.log(`[dsh-advisor][patrol] 巡逻调用失败(已跳过本轮):${outcome.errorMessage}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const verdict = parsePatrolVerdict(stripExecutorEcho(outcome.text, messages));
|
|
87
|
+
const elapsed = Math.round(Date.now() - startedAt);
|
|
88
|
+
if (verdict.kind === 'on-track') {
|
|
89
|
+
console.log(`[dsh-advisor][patrol] ON-TRACK(${elapsed}ms)——执行方向正常`);
|
|
90
|
+
// 低调灰卡:on-track 也写事件(用户拍板的完整视野);写失败不影响巡逻
|
|
91
|
+
appendPatrolVerdict(agent.session, {
|
|
92
|
+
verdict: 'on-track',
|
|
93
|
+
detail: verdict.detail,
|
|
94
|
+
step: stepCounts.get(agent) ?? 0,
|
|
95
|
+
reviewer: `${selection.provider}/${selection.model}`,
|
|
96
|
+
patrolMs: elapsed,
|
|
97
|
+
usage: outcome.usage,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (verdict.kind === 'unclear') {
|
|
102
|
+
// 解析失败不出卡片(不是"检查通过"也不是"需要干预",只留日志)
|
|
103
|
+
console.log(`[dsh-advisor][patrol] 裁决不可解析(${elapsed}ms),不打扰执行模型。原文前 200 字:${outcome.text.slice(0, 200)}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log(`[dsh-advisor][patrol] ${verdict.kind === 'stop' ? 'STOP' : 'CORRECTION'}(${elapsed}ms):${verdict.detail.slice(0, 200)}`);
|
|
107
|
+
// 防噪闸 ①:内容空短语——只给结论不给理由的建议没有信息量,按不打扰处理
|
|
108
|
+
const normalized = normalizeAdvice(verdict.detail);
|
|
109
|
+
if (isContentFreeAdvice(normalized)) {
|
|
110
|
+
console.log(`[dsh-advisor][patrol] 裁决内容空(归一化后无实质信息),不打扰执行模型:${normalized.slice(0, 60)}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const deduper = dedupers.get(agent) ?? createAdviceDeduper();
|
|
114
|
+
dedupers.set(agent, deduper);
|
|
115
|
+
// 冷却降级:一次注入后 N 个请求内,correction 只出卡片不注入(stop 豁免)
|
|
116
|
+
const step = stepCounts.get(agent) ?? 0;
|
|
117
|
+
const downgraded = verdict.kind === 'correction' && step < (immuneUntilStep.get(agent) ?? -1);
|
|
118
|
+
// 人类可观测性通道:裁决写入会话日志,消息流将出现 🧭 卡片(log-only)
|
|
119
|
+
appendPatrolVerdict(agent.session, {
|
|
120
|
+
verdict: verdict.kind,
|
|
121
|
+
detail: verdict.detail,
|
|
122
|
+
step,
|
|
123
|
+
reviewer: `${selection.provider}/${selection.model}`,
|
|
124
|
+
patrolMs: elapsed,
|
|
125
|
+
usage: outcome.usage,
|
|
126
|
+
...(downgraded ? { downgraded: true } : {}),
|
|
127
|
+
});
|
|
128
|
+
console.log('[dsh-advisor][patrol] 裁决已写入会话事件');
|
|
129
|
+
// 防噪闸 ②:会话内精确去重——与已注入过的建议重复则丢弃(只记日志)
|
|
130
|
+
if (!downgraded && !deduper(normalized)) {
|
|
131
|
+
console.log('[dsh-advisor][patrol] 与此前已注入的纠偏重复,丢弃本次注入');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (downgraded) {
|
|
135
|
+
console.log(`[dsh-advisor][patrol] 纠偏冷却期内(第 ${step} 步 < ${immuneUntilStep.get(agent)}),本次只出卡片不注入`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// 模型面投递:agent.inject() 把干预排成下一步请求的尾部 user 上下文。
|
|
139
|
+
// 实测教训:系统提示动态段在几百条工具结果的会话里新近性不足,执行
|
|
140
|
+
// 模型"读到但不动";尾部上下文是文件变更通知/skill 内容同款官方通道,
|
|
141
|
+
// 位置就是模型注意力最集中的对话末尾,且 UI 会渲染成上下文行。
|
|
142
|
+
try {
|
|
143
|
+
const immuneTurns = deps.getConfig().patrolImmuneTurns ?? 3;
|
|
144
|
+
if (verdict.kind === 'correction' && immuneTurns > 0) {
|
|
145
|
+
immuneUntilStep.set(agent, step + immuneTurns);
|
|
146
|
+
}
|
|
147
|
+
agent.inject(createUserMessage({
|
|
148
|
+
content: [{ type: 'text', text: buildInterventionText(verdict.kind, verdict.detail) }],
|
|
149
|
+
source: {
|
|
150
|
+
kind: 'plugin',
|
|
151
|
+
plugin: 'dsh-advisor',
|
|
152
|
+
form: 'notice',
|
|
153
|
+
summary: `${verdict.kind === 'stop' ? '巡逻叫停' : '巡逻纠偏'}:${verdict.detail.slice(0, 60)}`,
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
console.log(`[dsh-advisor][patrol] 已将${verdict.kind === 'stop' ? '停止' : '纠偏'}干预排入下一步请求(尾部上下文)`);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// 注入失败不影响巡逻本身
|
|
160
|
+
console.warn('[dsh-advisor][patrol] 干预注入失败:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
inFlight = false;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const disposeRequest = ctx.on('agent/request', async (payload, next) => {
|
|
168
|
+
const config = await next();
|
|
169
|
+
try {
|
|
170
|
+
const agent = payload.agent;
|
|
171
|
+
const step = (stepCounts.get(agent) ?? 0) + 1;
|
|
172
|
+
stepCounts.set(agent, step);
|
|
173
|
+
const pluginConfig = deps.getConfig();
|
|
174
|
+
const blocked = isExecutorBlocked(pluginConfig, config.provider, config.model, config.reasoningEffort);
|
|
175
|
+
if (!shouldPatrol({
|
|
176
|
+
armed: deps.getSelection() !== undefined,
|
|
177
|
+
blocked,
|
|
178
|
+
step,
|
|
179
|
+
everySteps: pluginConfig.patrolEverySteps ?? 6,
|
|
180
|
+
now: Date.now(),
|
|
181
|
+
lastPatrolMs,
|
|
182
|
+
minIntervalMs: MIN_PATROL_INTERVAL_MS,
|
|
183
|
+
inFlight,
|
|
184
|
+
patrolEnabled: pluginConfig.patrolEnabled !== false,
|
|
185
|
+
}))
|
|
186
|
+
return config;
|
|
187
|
+
lastPatrolMs = Date.now();
|
|
188
|
+
void runPatrol(agent);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
// 巡逻绝不破坏用户的 turn
|
|
192
|
+
console.error('[dsh-advisor][patrol] 调度失败:', error);
|
|
193
|
+
}
|
|
194
|
+
return config;
|
|
195
|
+
});
|
|
196
|
+
return () => {
|
|
197
|
+
disposeRequest();
|
|
198
|
+
stepCounts.clear();
|
|
199
|
+
dedupers.clear();
|
|
200
|
+
immuneUntilStep.clear();
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-section —— 注入系统提示的升级守则段。
|
|
3
|
+
* 只在已选审查模型时注册——未武装的 advisor 零提示词成本。
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
import type { Config } from './config.js';
|
|
7
|
+
export declare function registerAdvisorSection(ctx: Context, config: Config): () => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-section —— 注入系统提示的升级守则段。
|
|
3
|
+
* 只在已选审查模型时注册——未武装的 advisor 零提示词成本。
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_GUIDELINES } from './advisor-prompt.js';
|
|
6
|
+
export function registerAdvisorSection(ctx, config) {
|
|
7
|
+
const guidelines = config.guidelines === undefined || config.guidelines.length === 0
|
|
8
|
+
? DEFAULT_GUIDELINES
|
|
9
|
+
: config.guidelines;
|
|
10
|
+
const text = ['## Advisor escalation', '', ...guidelines.map(g => `- ${g}`)].join('\n');
|
|
11
|
+
return ctx.systemPrompt.section({
|
|
12
|
+
name: 'advisor-guidelines',
|
|
13
|
+
order: 550,
|
|
14
|
+
text,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import { type Config, type Selection } from './config.js';
|
|
11
|
+
export interface SettingsWiring {
|
|
12
|
+
/** 设置服务状态描述(进 /advisor 命令输出与日志) */
|
|
13
|
+
info: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function wireSettings(ctx: Context, state: {
|
|
16
|
+
config: Config;
|
|
17
|
+
selection: Selection | undefined;
|
|
18
|
+
}, onConfigChange: () => void): SettingsWiring;
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import { Config as ConfigSchema, resolveSelection } from './config.js';
|
|
10
|
+
export function wireSettings(ctx, state, onConfigChange) {
|
|
11
|
+
const settings = ctx.get('settings');
|
|
12
|
+
if (settings === undefined) {
|
|
13
|
+
return { info: 'settings 服务不可用——advisor 配置固定为插件组合层' };
|
|
14
|
+
}
|
|
15
|
+
if (typeof settings.installSection === 'function') {
|
|
16
|
+
// 较新宿主:setSource 把"当前值来源"交给设置服务接管
|
|
17
|
+
let source = () => state.config;
|
|
18
|
+
settings.installSection(ctx, 'advisor', ConfigSchema, state.config, {
|
|
19
|
+
setSource: (next) => {
|
|
20
|
+
source = next;
|
|
21
|
+
},
|
|
22
|
+
onChange: () => {
|
|
23
|
+
state.config = source();
|
|
24
|
+
state.selection = resolveSelection(state.config);
|
|
25
|
+
onConfigChange();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return { info: 'settings.installSection 已接入(advisor 命名空间可在设置页编辑)' };
|
|
29
|
+
}
|
|
30
|
+
if (typeof settings.register === 'function') {
|
|
31
|
+
// rc.6 宿主:scope.get() 读合成值(schema 默认 → base → 用户层),watch 活编辑
|
|
32
|
+
try {
|
|
33
|
+
const scope = settings.register('advisor', ConfigSchema, { base: state.config });
|
|
34
|
+
const resolved = scope.get();
|
|
35
|
+
state.selection = resolveSelection(resolved);
|
|
36
|
+
state.config = resolved;
|
|
37
|
+
onConfigChange();
|
|
38
|
+
scope.watch(next => {
|
|
39
|
+
state.config = next;
|
|
40
|
+
state.selection = resolveSelection(next);
|
|
41
|
+
onConfigChange();
|
|
42
|
+
});
|
|
43
|
+
return { info: `settings.register 已接入(advisor 命名空间,用户层在 ~/.dsh/settings.yaml)` };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { info: `settings.register 失败:${error instanceof Error ? error.message : String(error)}` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { info: 'settings 服务形态未知——advisor 配置固定为插件组合层' };
|
|
50
|
+
}
|
package/lib/tool.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool —— 零参数 `advisor` 工具(defineTool)。
|
|
3
|
+
*
|
|
4
|
+
* execute() 解析当前选择 → 组装审查消息 → 侧调用 → 返回规范值;
|
|
5
|
+
* 每条失败路径同样返回值(渲染成可读文本),执行模型读到错误说明后
|
|
6
|
+
* 继续本轮——advisor 挂掉绝不炸掉执行模型的 turn。
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
10
|
+
import { type Selection } from './config.js';
|
|
11
|
+
/** 工具结果信封:成功带指导文本,失败带可读错误(渲染层只取文本) */
|
|
12
|
+
export interface AdvisorValue {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
advisorModel: string;
|
|
15
|
+
guidance?: string;
|
|
16
|
+
effort?: string;
|
|
17
|
+
finishKind?: string;
|
|
18
|
+
errorMessage?: string;
|
|
19
|
+
inputTokens?: number;
|
|
20
|
+
outputTokens?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function createAdvisorTool(ctx: Context, getSelection: () => Selection | undefined, buildMessages: (agent: Agent) => import('@deepseek-ai/dsh-llm').Message[], getConfig: () => import('./config.js').Config): import("@deepseek-ai/dsh-tools").ToolDefinition;
|
package/lib/tool.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool —— 零参数 `advisor` 工具(defineTool)。
|
|
3
|
+
*
|
|
4
|
+
* execute() 解析当前选择 → 组装审查消息 → 侧调用 → 返回规范值;
|
|
5
|
+
* 每条失败路径同样返回值(渲染成可读文本),执行模型读到错误说明后
|
|
6
|
+
* 继续本轮——advisor 挂掉绝不炸掉执行模型的 turn。
|
|
7
|
+
*/
|
|
8
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
9
|
+
import { ADVISOR_SYSTEM_PROMPT, ADVISOR_TOOL_DESCRIPTION, ADVISOR_TOOL_NAME, MSG_NO_AGENT_SCOPE, MSG_NO_MODEL } from './advisor-prompt.js';
|
|
10
|
+
import { selectionLabel } from './config.js';
|
|
11
|
+
import { callReviewer, stripExecutorEcho } from './llm-call.js';
|
|
12
|
+
function renderText(value) {
|
|
13
|
+
if (!value.ok)
|
|
14
|
+
return value.errorMessage ?? 'Advisor call failed.';
|
|
15
|
+
return value.guidance ?? '';
|
|
16
|
+
}
|
|
17
|
+
/** 从工具结果内容块提取纯文本(presentResult 只拿得到渲染后的内容块) */
|
|
18
|
+
function textOfContent(content) {
|
|
19
|
+
return content
|
|
20
|
+
.filter((block) => block.type === 'text')
|
|
21
|
+
.map(block => block.text)
|
|
22
|
+
.join('\n')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
|
25
|
+
export function createAdvisorTool(ctx, getSelection, buildMessages, getConfig) {
|
|
26
|
+
return defineTool({
|
|
27
|
+
name: ADVISOR_TOOL_NAME,
|
|
28
|
+
description: ADVISOR_TOOL_DESCRIPTION,
|
|
29
|
+
parameters: {},
|
|
30
|
+
output: {
|
|
31
|
+
schema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
properties: {
|
|
35
|
+
ok: { type: 'boolean' },
|
|
36
|
+
advisorModel: { type: 'string' },
|
|
37
|
+
guidance: { type: 'string' },
|
|
38
|
+
effort: { type: 'string' },
|
|
39
|
+
finishKind: { type: 'string' },
|
|
40
|
+
errorMessage: { type: 'string' },
|
|
41
|
+
inputTokens: { type: 'number' },
|
|
42
|
+
outputTokens: { type: 'number' },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
render: (_args, value) => [{ type: 'text', text: renderText(value) }],
|
|
46
|
+
},
|
|
47
|
+
// 专属卡片:调用中/结果两态都有 🧭 标识,结果卡片正文就是审查建议,
|
|
48
|
+
// 用户在消息列表展开即见"真的调了 advisor、建议是什么"
|
|
49
|
+
presentCall: () => ({ card: 'generic', title: '🧭 Advisor 咨询中…' }),
|
|
50
|
+
presentResult: (_args, result) => {
|
|
51
|
+
const text = textOfContent(result.content);
|
|
52
|
+
return {
|
|
53
|
+
card: 'generic',
|
|
54
|
+
title: result.isError ? '🧭 Advisor 调用失败' : '🧭 Advisor 建议',
|
|
55
|
+
...(text === '' ? {} : { content: [{ type: 'text', text }] }),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
async execute(_args, exec) {
|
|
59
|
+
const selection = getSelection();
|
|
60
|
+
if (selection === undefined) {
|
|
61
|
+
return { ok: false, advisorModel: '(not configured)', errorMessage: MSG_NO_MODEL };
|
|
62
|
+
}
|
|
63
|
+
if (exec.agent === undefined) {
|
|
64
|
+
return { ok: false, advisorModel: selectionLabel(selection), errorMessage: MSG_NO_AGENT_SCOPE };
|
|
65
|
+
}
|
|
66
|
+
const messages = buildMessages(exec.agent);
|
|
67
|
+
// 调查授权与巡逻一致:审查模型可先只读核实再给建议
|
|
68
|
+
const investigate = getConfig().investigate !== false ? exec.agent.session.header.cwd : undefined;
|
|
69
|
+
const outcome = await callReviewer(ctx, selection, ADVISOR_SYSTEM_PROMPT, messages, exec.signal, investigate, exec.agent.session.id);
|
|
70
|
+
if (!outcome.ok) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
advisorModel: selectionLabel(selection),
|
|
74
|
+
...(selection.effort === undefined ? {} : { effort: selection.effort }),
|
|
75
|
+
errorMessage: outcome.errorMessage,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
advisorModel: selectionLabel(selection),
|
|
81
|
+
// 剥掉审查模型对执行模型"最后一句话"的回显(网关实测行为)
|
|
82
|
+
guidance: stripExecutorEcho(outcome.text, messages),
|
|
83
|
+
...(selection.effort === undefined ? {} : { effort: selection.effort }),
|
|
84
|
+
finishKind: outcome.finishKind,
|
|
85
|
+
...(outcome.usage === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { inputTokens: outcome.usage.inputTokens, outputTokens: outcome.usage.outputTokens }),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-advisor-plugin",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DeepSeek Harness advisor plugin: pair a fast executor with a stronger reviewer model for plan / correction / stop guidance.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"types": "./lib/client/index.d.ts",
|
|
15
|
+
"default": "./lib/client/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./src/*": "./src/*",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"dsh": {
|
|
21
|
+
"client": {
|
|
22
|
+
"inject": [
|
|
23
|
+
"@deepseek-ai/dsh-client-locale",
|
|
24
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
25
|
+
"@deepseek-ai/dsh-client-connection",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-cordis"
|
|
27
|
+
],
|
|
28
|
+
"platform": "web"
|
|
29
|
+
},
|
|
30
|
+
"bundle": {
|
|
31
|
+
"patch": "./cordis.patch.yml"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"lib",
|
|
37
|
+
"cordis.patch.yml",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"registry": "https://registry.npmjs.org",
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/leeyoung1/dsh-advisor-plugin.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/leeyoung1/dsh-advisor-plugin",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/leeyoung1/dsh-advisor-plugin/issues"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"deepseek-harness",
|
|
58
|
+
"dsh",
|
|
59
|
+
"dsh-plugin",
|
|
60
|
+
"advisor",
|
|
61
|
+
"agent",
|
|
62
|
+
"reviewer",
|
|
63
|
+
"coding-agent"
|
|
64
|
+
],
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@deepseek-ai/cordis": "*",
|
|
67
|
+
"@deepseek-ai/dsh-agent": "*",
|
|
68
|
+
"@deepseek-ai/dsh-commands": "*",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
70
|
+
"@deepseek-ai/dsh-session": "*",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
72
|
+
"@deepseek-ai/dsh-system-prompt": "*",
|
|
73
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
74
|
+
"@deepseek-ai/schemastery": "*"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@types/node": "^24.0.0",
|
|
78
|
+
"@types/react": "~18.3.1",
|
|
79
|
+
"esbuild": "^0.28.2",
|
|
80
|
+
"react": "^18.2.0",
|
|
81
|
+
"typescript": "^5.6.0",
|
|
82
|
+
"vitest": "^3.0.0"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsc -p tsconfig.build.json && node scripts/build-client.mjs",
|
|
86
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"prepare": "npm run build",
|
|
89
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor-prompt —— 插件全部固定文案与提示词常量(纯声明,无逻辑)。
|
|
3
|
+
*
|
|
4
|
+
* 面向模型的文本用英文(模型侧语义稳定),面向用户的日志用中文。
|
|
5
|
+
* 三个提示词共同构成 advisor 策略模式契约:审查模型三选一回文、
|
|
6
|
+
* 执行模型按守则升级、尾部 nudge 保证 user 结尾。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ADVISOR_TOOL_NAME = 'advisor'
|
|
10
|
+
|
|
11
|
+
/** 发给审查模型的系统提示词:plan / correction / stop signal 三选一契约 */
|
|
12
|
+
export const ADVISOR_SYSTEM_PROMPT = [
|
|
13
|
+
'You are an advisor model in an advisor-strategy pattern.',
|
|
14
|
+
'An executor model is running a task end-to-end — calling tools, reading',
|
|
15
|
+
'results, iterating toward a solution. When the executor hits a decision it',
|
|
16
|
+
'cannot reasonably solve alone, it consults you for guidance. The executor\'s',
|
|
17
|
+
'full tool inventory is prepended before the conversation so you can judge',
|
|
18
|
+
'tool-choice correctness.',
|
|
19
|
+
'',
|
|
20
|
+
'You read the shared conversation context and return ONE of:',
|
|
21
|
+
'- a plan (concrete next steps the executor should take),',
|
|
22
|
+
'- a correction (the executor is going down a wrong path — redirect it),',
|
|
23
|
+
'- a stop signal (the executor should halt and escalate to the user).',
|
|
24
|
+
'',
|
|
25
|
+
'You NEVER call tools. You NEVER produce user-facing output. Be concise,',
|
|
26
|
+
'directive, and grounded in the shared context. Name files, functions, and',
|
|
27
|
+
'line numbers where possible. No preamble, no apologies, no meta-commentary',
|
|
28
|
+
'— just the guidance the executor needs.',
|
|
29
|
+
].join('\n')
|
|
30
|
+
|
|
31
|
+
/** 零参数工具 description —— 执行模型判断"何时升级"的主要依据 */
|
|
32
|
+
export const ADVISOR_TOOL_DESCRIPTION = [
|
|
33
|
+
'Escalate to a stronger reviewer model for guidance. When you need',
|
|
34
|
+
'stronger judgment — a complex decision, an ambiguous failure, a problem',
|
|
35
|
+
"you're circling without progress — escalate to the advisor model for",
|
|
36
|
+
'guidance, then resume. Takes NO parameters — when you call advisor(),',
|
|
37
|
+
'your entire conversation history (task, every tool call, every result)',
|
|
38
|
+
'is automatically forwarded. When to call:',
|
|
39
|
+
'(1) BEFORE substantive work — before writing, before committing to an',
|
|
40
|
+
'interpretation, before building on an assumption;',
|
|
41
|
+
'(2) BEFORE declaring done — and make your deliverable durable first;',
|
|
42
|
+
'(3) when stuck — recurring errors, approach not converging;',
|
|
43
|
+
'(4) when your evidence contradicts previous advice, reconcile in one',
|
|
44
|
+
'more call instead of silently switching.',
|
|
45
|
+
'After each advisor result, restate its key guidance (plan / correction',
|
|
46
|
+
'/ stop signal) in your next visible reply to the user before continuing.',
|
|
47
|
+
].join(' ')
|
|
48
|
+
|
|
49
|
+
/** 注入系统提示的升级守则(未配置审查模型时不注册,零提示词成本) */
|
|
50
|
+
export const DEFAULT_GUIDELINES: readonly string[] = [
|
|
51
|
+
'Call `advisor` BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (finding files, fetching a source, seeing what is there) is not substantive work; writing, editing, and declaring an answer are.',
|
|
52
|
+
'Also call `advisor` when you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one does not.',
|
|
53
|
+
'Also call `advisor` when stuck — errors recurring, approach not converging, results that do not fit — or when considering a change of approach.',
|
|
54
|
+
'On tasks longer than a few steps, call `advisor` at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don\'t need to keep calling.',
|
|
55
|
+
'Give the advisor\'s advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim, adapt — a passing self-test is not evidence the advice is wrong.',
|
|
56
|
+
'If you\'ve already retrieved data pointing one way and the advisor points another, don\'t silently switch — surface the conflict in one more `advisor` call. A reconcile call is cheaper than committing to the wrong branch.',
|
|
57
|
+
'After each `advisor` result, put the advisor\'s key guidance into your next visible reply to the user before continuing — quote or paraphrase the plan, correction, or stop signal. The user often cannot see collapsed tool results.',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** 尾部 nudge:部分 provider 拒绝 assistant 结尾的载荷 */
|
|
61
|
+
export const MSG_USER_TAIL_NUDGE = 'Please advise on the executor\'s situation above.'
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 巡逻模式系统提示:审查模型对执行过程做中段检查,回三选一裁决。
|
|
65
|
+
* 首行前缀是机器可解析契约(patrol.parsePatrolVerdict),细节跟在后面。
|
|
66
|
+
*/
|
|
67
|
+
export const PATROL_SYSTEM_PROMPT = [
|
|
68
|
+
'You are the advisor model running a PATROL check on an executor model mid-task.',
|
|
69
|
+
'You see the executor\'s tool inventory and its conversation so far.',
|
|
70
|
+
'Reply with EXACTLY one verdict on the first line, detail after it if applicable:',
|
|
71
|
+
'- "ON-TRACK" — the executor is progressing correctly; reply with just this line',
|
|
72
|
+
'- "CORRECTION: <one-paragraph redirect>" — the executor has drifted; state what is wrong and the concrete next step',
|
|
73
|
+
'- "STOP: <reason>" — the executor should halt and report to the user',
|
|
74
|
+
'Flag only substantive drift: wrong files or approach, hallucinated progress,',
|
|
75
|
+
'destructive operations, violating the user\'s constraints, or the task already',
|
|
76
|
+
'being satisfied while the executor keeps working. Do not nitpick style.',
|
|
77
|
+
].join('\n')
|
|
78
|
+
|
|
79
|
+
/** 巡逻问题(挂在快照尾部,随整段会话发给审查模型) */
|
|
80
|
+
export const PATROL_NUDGE = 'Patrol check: is the executor still on track for the user\'s task? Reply per the patrol contract.'
|
|
81
|
+
|
|
82
|
+
/** 把巡逻裁决转成注入执行模型的干预文本(llm/stream 尾部 user 消息) */
|
|
83
|
+
export function buildInterventionText(kind: 'correction' | 'stop', detail: string): string {
|
|
84
|
+
if (kind === 'stop') {
|
|
85
|
+
return [
|
|
86
|
+
'[advisor patrol] STOP signal from the reviewer model that monitors this execution:',
|
|
87
|
+
detail,
|
|
88
|
+
'Halt the current approach immediately and report the situation to the user.',
|
|
89
|
+
].join('\n')
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
'[advisor patrol] The reviewer model monitoring this execution flagged a drift. Its correction:',
|
|
93
|
+
detail,
|
|
94
|
+
'Adjust your approach now. Briefly acknowledge this correction in your next visible reply, then continue.',
|
|
95
|
+
].join('\n')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 用户可见文案(中文) */
|
|
99
|
+
export const MSG_NO_MODEL =
|
|
100
|
+
'未配置 advisor 审查模型——在设置 → 插件里选择一个审查模型(或 /advisor 查看可用路由)。'
|
|
101
|
+
export const MSG_NO_AGENT_SCOPE =
|
|
102
|
+
'advisor 无法访问当前会话(未拿到 agent 上下文)——请从会话内调用。'
|