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/lib/gating.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * gating —— 按 agent 动态可见性。每次解析模型请求时把执行路由对照
3
+ * 黑名单:命中的 agent 在自己的作用域上挂 restrict({deny:['advisor']})
4
+ * (工具 schema 从该 agent 的提示里消失),未命中的解除限制。
5
+ */
6
+ import { ADVISOR_TOOL_NAME } from './advisor-prompt.js';
7
+ import { isExecutorBlocked } from './config.js';
8
+ export function registerGating(ctx, getConfig, hasReviewer) {
9
+ const restrictions = new Map();
10
+ const lift = (agent) => {
11
+ const dispose = restrictions.get(agent);
12
+ if (dispose !== undefined) {
13
+ dispose();
14
+ restrictions.delete(agent);
15
+ }
16
+ };
17
+ const reconcile = (agent, provider, model, reasoningEffort) => {
18
+ // 未武装时工具根本没注册——无事可藏,且 restrict() 会拒绝未知名
19
+ if (!hasReviewer()) {
20
+ lift(agent);
21
+ return;
22
+ }
23
+ const blocked = isExecutorBlocked(getConfig(), provider, model, reasoningEffort);
24
+ if (blocked && !restrictions.has(agent)) {
25
+ restrictions.set(agent, agent.ctx.tools.restrict({ deny: [ADVISOR_TOOL_NAME] }));
26
+ }
27
+ else if (!blocked) {
28
+ lift(agent);
29
+ }
30
+ };
31
+ const disposeListener = ctx.on('agent/request', async (payload, next) => {
32
+ const config = await next();
33
+ try {
34
+ reconcile(payload.agent, config.provider, config.model, config.reasoningEffort);
35
+ }
36
+ catch (error) {
37
+ // 可见性门控绝不破坏用户的 turn
38
+ console.error('[dsh-advisor] gating 失败:', error);
39
+ }
40
+ return config;
41
+ });
42
+ return () => {
43
+ disposeListener();
44
+ for (const dispose of restrictions.values())
45
+ dispose();
46
+ restrictions.clear();
47
+ };
48
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * history —— 组装发给审查模型的消息列表。
3
+ *
4
+ * 结构(对齐 rpiv-advisor):
5
+ * [工具清单合成消息] + [session.deriveMessages() 的模型可见面]
6
+ *
7
+ * 两个工程要点:
8
+ * 1. deriveMessages() 是 compaction 感知的——压缩摘要按模型实际看到的
9
+ * 面貌转发,而不是重放压缩前的原始历史;
10
+ * 2. 工具清单做"按名排序 + 键排序稳定序列化"——多次 advisor 调用间
11
+ * 字节级一致,命中 DeepSeek 上下文缓存(缓存是整段转发模式的省钱杠杆)。
12
+ *
13
+ * 尾部两条规则原样移植 rpiv:剥掉 in-flight 的 advisor() 调用(孤儿
14
+ * toolCall 会被 provider 拒绝);保证 user 结尾(部分 provider 拒绝
15
+ * assistant 结尾)。
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { Agent } from '@deepseek-ai/dsh-agent';
19
+ import { type Message } from '@deepseek-ai/dsh-llm';
20
+ export declare function createHistoryBuilder(ctx: Context): (agent: Agent) => Message[];
package/lib/history.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * history —— 组装发给审查模型的消息列表。
3
+ *
4
+ * 结构(对齐 rpiv-advisor):
5
+ * [工具清单合成消息] + [session.deriveMessages() 的模型可见面]
6
+ *
7
+ * 两个工程要点:
8
+ * 1. deriveMessages() 是 compaction 感知的——压缩摘要按模型实际看到的
9
+ * 面貌转发,而不是重放压缩前的原始历史;
10
+ * 2. 工具清单做"按名排序 + 键排序稳定序列化"——多次 advisor 调用间
11
+ * 字节级一致,命中 DeepSeek 上下文缓存(缓存是整段转发模式的省钱杠杆)。
12
+ *
13
+ * 尾部两条规则原样移植 rpiv:剥掉 in-flight 的 advisor() 调用(孤儿
14
+ * toolCall 会被 provider 拒绝);保证 user 结尾(部分 provider 拒绝
15
+ * assistant 结尾)。
16
+ */
17
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
18
+ import { ADVISOR_TOOL_NAME, MSG_USER_TAIL_NUDGE } from './advisor-prompt.js';
19
+ // 递归键排序序列化:键序与 V8 插入序无关,同一清单字节级一致
20
+ function stableStringify(value) {
21
+ if (value === null || typeof value !== 'object')
22
+ return JSON.stringify(value);
23
+ if (Array.isArray(value)) {
24
+ return `[${value.map(v => (v === undefined ? 'null' : stableStringify(v))).join(',')}]`;
25
+ }
26
+ const obj = value;
27
+ const entries = [];
28
+ for (const k of Object.keys(obj).sort()) {
29
+ const v = obj[k];
30
+ if (v === undefined)
31
+ continue;
32
+ entries.push(`${JSON.stringify(k)}:${stableStringify(v)}`);
33
+ }
34
+ return `{${entries.join(',')}}`;
35
+ }
36
+ function createUserText(text) {
37
+ return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } });
38
+ }
39
+ function getInventoryMessage(ctx, cache, scope) {
40
+ // 传 agent 作用域:审查者看到的必须是执行模型实际可见的工具面
41
+ // (黑名单 restrict、agent-scoped 工具都体现在这个 scope 里)
42
+ const schemas = ctx.tools.schemas(scope);
43
+ if (schemas.length === 0)
44
+ return undefined;
45
+ const sorted = [...schemas].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
46
+ const signature = sorted.map(t => t.name).join('|');
47
+ if (cache.signature === signature && cache.message !== undefined)
48
+ return cache.message;
49
+ const block = sorted
50
+ .map(t => `### ${t.name}\n${t.description}\n\nParameters: ${stableStringify(t.parameters)}`)
51
+ .join('\n\n---\n\n');
52
+ const message = createUserText(`## Available Executor Tools\n\n${block}`);
53
+ cache.signature = signature;
54
+ cache.message = message;
55
+ return message;
56
+ }
57
+ // 剥掉尾部 assistant 消息里 in-flight 的 advisor() toolCall——正是触发本次
58
+ // 咨询的那个调用,还没有配对结果,转发它只会让 provider 拒绝载荷
59
+ function stripInflightAdvisorCall(messages) {
60
+ if (messages.length === 0)
61
+ return messages;
62
+ const last = messages[messages.length - 1];
63
+ if (last === undefined || last.role !== 'assistant')
64
+ return messages;
65
+ const filtered = last.content.filter(block => !(block.type === 'tool-call' && block.name === ADVISOR_TOOL_NAME));
66
+ if (filtered.length === last.content.length)
67
+ return messages;
68
+ if (filtered.length === 0)
69
+ return messages.slice(0, -1);
70
+ return [...messages.slice(0, -1), { ...last, content: filtered }];
71
+ }
72
+ // 保证 user 结尾:剥除后尾部可能是 assistant(executor 在调用前输出了思考)
73
+ function ensureUserTail(messages) {
74
+ if (messages.length === 0)
75
+ return messages;
76
+ const last = messages[messages.length - 1];
77
+ if (last === undefined || last.role !== 'assistant')
78
+ return messages;
79
+ return [...messages, createUserText(MSG_USER_TAIL_NUDGE)];
80
+ }
81
+ export function createHistoryBuilder(ctx) {
82
+ // 每个 agent 一份清单缓存(WeakMap 不阻碍临时 agent 回收):工具可见面
83
+ // 是 per-agent 的,作用域不同清单也不同,不能共用同一份缓存
84
+ const caches = new WeakMap();
85
+ return function buildAdvisorMessages(agent) {
86
+ const branch = ensureUserTail(stripInflightAdvisorCall(agent.session.deriveMessages()));
87
+ let cache = caches.get(agent);
88
+ if (cache === undefined) {
89
+ cache = {};
90
+ caches.set(agent, cache);
91
+ }
92
+ const inventory = getInventoryMessage(ctx, cache, agent);
93
+ return inventory === undefined ? branch : [inventory, ...branch];
94
+ };
95
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * dsh-advisor —— advisor 策略模式的 DSH 实现(host 半入口)。
3
+ *
4
+ * 机制(rpiv-advisor 移植):
5
+ * 1. 执行模型拿到零参数 `advisor` 工具;调用即把整段会话分支
6
+ * (compaction 感知)+ 工具清单转发给配置的更强审查模型;
7
+ * 2. 审查模型按 plan / correction / stop signal 三选一契约回文,
8
+ * 作为工具结果交还执行模型——不进人类可见对话流;
9
+ * 3. 组合关系:选了审查模型 ⇔ 工具 + 升级守则提示段注册在案
10
+ * (未武装的 advisor 零提示词成本);
11
+ * 4. agent/request 监听每次解析的执行路由,命中 disabledForModels
12
+ * 黑名单时对那个 agent 作用域挂 restrict 隐藏工具;
13
+ * 5. settings 服务就绪后配置接入 'advisor' 命名空间(用户层在
14
+ * ~/.dsh/settings.yaml 的 advisor: 段;设置页卡片实时读写)。
15
+ *
16
+ * 服务时序:tools/llm/systemPrompt 是硬依赖(inject 导出);settings 与
17
+ * commands 是软依赖——用 ctx.inject 等待就绪,apply 时刻 ctx.get
18
+ * 拿到 undefined 只说明服务尚未激活,不代表不存在。
19
+ */
20
+ import type { Context } from '@deepseek-ai/cordis';
21
+ import { Config as ConfigSchema, type Config } from './config.js';
22
+ export declare const name = "dsh-advisor";
23
+ export declare const inject: string[];
24
+ export { ConfigSchema as Config };
25
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * dsh-advisor —— advisor 策略模式的 DSH 实现(host 半入口)。
3
+ *
4
+ * 机制(rpiv-advisor 移植):
5
+ * 1. 执行模型拿到零参数 `advisor` 工具;调用即把整段会话分支
6
+ * (compaction 感知)+ 工具清单转发给配置的更强审查模型;
7
+ * 2. 审查模型按 plan / correction / stop signal 三选一契约回文,
8
+ * 作为工具结果交还执行模型——不进人类可见对话流;
9
+ * 3. 组合关系:选了审查模型 ⇔ 工具 + 升级守则提示段注册在案
10
+ * (未武装的 advisor 零提示词成本);
11
+ * 4. agent/request 监听每次解析的执行路由,命中 disabledForModels
12
+ * 黑名单时对那个 agent 作用域挂 restrict 隐藏工具;
13
+ * 5. settings 服务就绪后配置接入 'advisor' 命名空间(用户层在
14
+ * ~/.dsh/settings.yaml 的 advisor: 段;设置页卡片实时读写)。
15
+ *
16
+ * 服务时序:tools/llm/systemPrompt 是硬依赖(inject 导出);settings 与
17
+ * commands 是软依赖——用 ctx.inject 等待就绪,apply 时刻 ctx.get
18
+ * 拿到 undefined 只说明服务尚未激活,不代表不存在。
19
+ */
20
+ import { Config as ConfigSchema, resolveSelection } from './config.js';
21
+ import { registerAdvisorCommand } from './command.js';
22
+ import { registerGating } from './gating.js';
23
+ import { createHistoryBuilder } from './history.js';
24
+ import { registerPatrol } from './patrol.js';
25
+ import { registerAdvisorSection } from './prompt-section.js';
26
+ import { createAdvisorTool } from './tool.js';
27
+ import { wireSettings } from './settings.js';
28
+ export const name = 'dsh-advisor';
29
+ export const inject = ['tools', 'llm', 'systemPrompt'];
30
+ export { ConfigSchema as Config };
31
+ export function apply(ctx, config) {
32
+ const state = {
33
+ config,
34
+ selection: resolveSelection(config),
35
+ settingsInfo: 'settings 服务未接入',
36
+ };
37
+ const buildMessages = createHistoryBuilder(ctx);
38
+ let registration;
39
+ // 武装/解除的总闸:工具与提示段同生同灭
40
+ const reconcileRegistration = () => {
41
+ registration?.();
42
+ registration = undefined;
43
+ if (state.selection === undefined)
44
+ return;
45
+ const disposeTool = ctx.tools.register(createAdvisorTool(ctx, () => state.selection, buildMessages, () => state.config));
46
+ const disposeSection = registerAdvisorSection(ctx, state.config);
47
+ registration = () => {
48
+ disposeTool();
49
+ disposeSection();
50
+ };
51
+ };
52
+ const disposeGating = registerGating(ctx, () => state.config, () => state.selection !== undefined);
53
+ // 巡逻模式:按间隔自动检查执行是否跑偏并注入纠偏(未武装/被禁用时内部 no-op)
54
+ const disposePatrol = registerPatrol(ctx, {
55
+ getConfig: () => state.config,
56
+ getSelection: () => state.selection,
57
+ buildMessages,
58
+ });
59
+ if (state.config.patrolEnabled !== false) {
60
+ console.log(`[dsh-advisor] 巡逻模式开启:每 ${state.config.patrolEverySteps ?? 6} 步自动检查(间隔下限 90s)`);
61
+ }
62
+ // /advisor 命令:commands 服务就绪后注册(fiber 卸载自动清理)
63
+ ctx.inject(['commands'], (commandsCtx) => {
64
+ const dispose = registerAdvisorCommand(commandsCtx, () => state.config, () => state.selection, () => state.settingsInfo);
65
+ if (dispose !== undefined)
66
+ commandsCtx.effect(() => dispose, 'dsh-advisor: /advisor command');
67
+ });
68
+ // 设置接入:settings 服务就绪后把 'advisor' 命名空间接进来并活编辑。
69
+ // 注意传插件根 ctx:rc.6 cordis 的 Service 方法绑定调用者 fiber,注册
70
+ // effect 挂在 inject 的临时 fiber 上会在 fiber 回收时静默注销命名空间。
71
+ ctx.inject(['settings'], () => {
72
+ const wiring = wireSettings(ctx, state, reconcileRegistration);
73
+ state.settingsInfo = wiring.info;
74
+ reconcileRegistration();
75
+ console.log(`[dsh-advisor] ${wiring.info}`);
76
+ // 设置解析后的最终武装状态(apply 时刻的那条"未武装"只是组合层初值)
77
+ if (state.selection === undefined) {
78
+ console.log('[dsh-advisor] 设置解析完成:仍未武装(advisor: 段缺 provider/model)——工具不注册');
79
+ }
80
+ else {
81
+ console.log(`[dsh-advisor] 设置解析完成:已武装 ${state.selection.provider}/${state.selection.model}${state.selection.effort === undefined ? '' : ` (${state.selection.effort})`}——advisor 工具已注册`);
82
+ }
83
+ });
84
+ reconcileRegistration();
85
+ if (state.selection === undefined) {
86
+ console.log('[dsh-advisor] 未武装:未配置审查模型(provider+model),advisor 工具不注册');
87
+ }
88
+ else {
89
+ console.log(`[dsh-advisor] 已武装:${state.selection.provider}/${state.selection.model}${state.selection.effort === undefined ? '' : ` (${state.selection.effort})`}`);
90
+ }
91
+ ctx.effect(() => () => {
92
+ registration?.();
93
+ disposeGating();
94
+ disposePatrol();
95
+ });
96
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * llm-call —— 审查模型侧调用,走 ctx.llm.stream(复用 DSH 已注册的
3
+ * provider 路由与凭据——审查模型就是"DSH 已经配置好的模型"之一)。
4
+ *
5
+ * 聚合 text-delta、捕获 usage、把 finish 分块路由成类型化结果;
6
+ * 正常停止但无正文时恰好重试一次(aborted / error 短路不重试)。
7
+ *
8
+ * 调查工具(对照 oh-my-pi:advisor 默认授予 read/grep/glob,建议前先
9
+ * 亲自查证):传入 investigateRoot 时走 agentic 循环——审查模型可用
10
+ * 两个只读工具(工作区内正则搜索 / 读文件)核实事实后再出最终裁决。
11
+ * 工具在本地用 node:fs 实现(只读、锁定在会话工作区子树内、结果截断),
12
+ * 不经过宿主工具运行时(无审批/沙箱副作用,代价是只有这两个只读原语)。
13
+ */
14
+ import type { Context } from '@deepseek-ai/cordis';
15
+ import type { Message, ToolCallBlock, TokenUsage } from '@deepseek-ai/dsh-llm';
16
+ import type { SessionId } from '@deepseek-ai/dsh-session';
17
+ import type { Selection } from './config.js';
18
+ export type AdvisorOutcome = {
19
+ ok: true;
20
+ text: string;
21
+ usage: TokenUsage | undefined;
22
+ finishKind: string;
23
+ } | {
24
+ ok: false;
25
+ errorMessage: string;
26
+ };
27
+ export declare function isReviewerCallActive(): boolean;
28
+ /**
29
+ * 剥离审查模型对执行模型"最后一句话"的回显。实测部分审查模型(经由
30
+ * 代理网关)会在正式建议前先复读执行模型调用 advisor 前的可见文本
31
+ * (如"好的,马上调用 advisor 咨询!")——对执行模型毫无信息量。
32
+ * 仅当 guidance 的首个非空行与转发消息里最后一条 assistant 文本完全
33
+ * 一致时剥掉该行,误伤面最小。
34
+ */
35
+ export declare function stripExecutorEcho(guidance: string, messages: readonly Message[]): string;
36
+ /**
37
+ * 审查侧调用入口。
38
+ * @param investigateRoot 提供时启用调查循环:审查模型可用只读工具在工作区
39
+ * 内核实后再出最终裁决(对照 oh-my-pi 的 advisor 调查授权)
40
+ */
41
+ export declare function callReviewer(ctx: Context, selection: Selection, systemPrompt: string, messages: Message[], signal: AbortSignal | undefined, investigateRoot?: string, sessionId?: SessionId): Promise<AdvisorOutcome>;
42
+ /** 是否"模型不支持该推理档位"类错误(纯函数,可单测) */
43
+ export declare function isUnsupportedEffortError(errorMessage: string | undefined): boolean;
44
+ /** 是否"上下文窗口溢出"类错误(纯函数,可单测) */
45
+ export declare function isContextOverflowError(errorMessage: string | undefined): boolean;
46
+ /**
47
+ * 溢出时的截断转发(纯函数,可单测):保留首条(工具清单合成消息)+
48
+ * 尾部约 1/4 的近期消息,并在衔接处插入一条截断说明。巡逻/咨询的裁决
49
+ * 主要依赖近期行为,截断后仍足以判断方向。
50
+ *
51
+ * 配对安全:消息边界截断可能把 tool_use 切在丢弃区、把配对的
52
+ * tool_result 留在保留区头部——孤儿 tool_result 会被 Claude/OpenAI 类
53
+ * provider 整包拒绝(实测 `unexpected tool_use_id ... must have a
54
+ * corresponding tool_use block`)。从保留区头部起剥掉含 tool-result 块
55
+ * 的 user 消息,直到首条不再引用被切断的调用。
56
+ */
57
+ export declare function truncateForReviewer(messages: readonly Message[]): Message[];
58
+ /** 简易 glob → RegExp(* 不跨目录、** 跨目录、双星号加斜杠匹配零个或多个目录、? 单字符) */
59
+ export declare function globToRegExp(glob: string): RegExp;
60
+ /**
61
+ * 把目标路径解析进工作区子树(纯函数,可单测):越界(../ 或绝对路径指向
62
+ * 区外)返回 undefined——审查者的读取面被硬限制在会话工作区内。
63
+ */
64
+ export declare function resolveWithinRoot(root: string, target: string): string | undefined;
65
+ /** 调查工具执行器(只读、限工作区、结果截断) */
66
+ export declare function createInvestigateExecutor(root: string): (call: ToolCallBlock) => Promise<{
67
+ content: string;
68
+ isError: boolean;
69
+ }>;