dsh-grill-first 0.1.3 → 0.1.5
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/CHANGELOG.md +37 -0
- package/LICENSE +21 -21
- package/README.en.md +167 -0
- package/README.md +60 -11
- package/cordis.patch.yml +4 -4
- package/dist/config.d.ts +6 -2
- package/dist/config.js +3 -1
- package/dist/configSchema.d.ts +4 -2
- package/dist/configSchema.js +8 -2
- package/dist/gate/preExecute.d.ts +14 -4
- package/dist/gate/preExecute.js +49 -2
- package/dist/index.js +13 -0
- package/dist/judge/heuristics.d.ts +12 -1
- package/dist/judge/heuristics.js +38 -7
- package/dist/memory/store.d.ts +14 -5
- package/dist/memory/store.js +20 -8
- package/dist/tools/grillConfirm.d.ts +6 -1
- package/dist/tools/grillConfirm.js +13 -2
- package/dist/tools/grillStart.d.ts +5 -1
- package/dist/tools/grillStart.js +27 -3
- package/dist/tools/sessions.d.ts +4 -0
- package/dist/tools/sessions.js +9 -1
- package/package.json +1 -1
- package/README.repo.md +0 -161
package/dist/gate/preExecute.js
CHANGED
|
@@ -50,10 +50,11 @@ export const READ_ONLY_TOOLS = [
|
|
|
50
50
|
'update_goal',
|
|
51
51
|
'grill_start',
|
|
52
52
|
'grill_confirm',
|
|
53
|
-
'grill_memory',
|
|
54
53
|
];
|
|
55
54
|
/** 拦截时给模型看的拒绝文案(工作表 3.6 原文)。 */
|
|
56
55
|
export const GATE_DENY_REASON = "需求尚未确认:请先调用 grill_confirm 完成需求确认,或让用户说'直接做'跳过。";
|
|
56
|
+
/** ask 档弹给用户看的说明(宿主用 displayReason 渲染卡片正文)。 */
|
|
57
|
+
export const GATE_ASK_REASON = '这次任务的需求还没确认,要照现在的理解直接动手吗?';
|
|
57
58
|
const READ_ONLY_SET = new Set(READ_ONLY_TOOLS.map((n) => n.toLowerCase()));
|
|
58
59
|
const MUTATING_SET = new Set(MUTATING_TOOLS.map((n) => n.toLowerCase()));
|
|
59
60
|
/** univer_* 里只有这几个是只读的,其余按写类处理。 */
|
|
@@ -81,14 +82,46 @@ export function isMutatingTool(name) {
|
|
|
81
82
|
* @param input 工具名 + 当前门禁状态 + 档位。
|
|
82
83
|
*/
|
|
83
84
|
export function decideGate(input) {
|
|
84
|
-
|
|
85
|
+
// off / warn 只记录不拦截(warn 的提示在 registerGate 里单独打日志)。
|
|
86
|
+
if (input.mode === 'off' || input.mode === 'warn')
|
|
85
87
|
return { action: 'allow' };
|
|
86
88
|
if (input.state !== 'closed')
|
|
87
89
|
return { action: 'allow' };
|
|
88
90
|
if (!isMutatingTool(input.toolName))
|
|
89
91
|
return { action: 'allow' };
|
|
92
|
+
if (input.mode === 'ask')
|
|
93
|
+
return { action: 'ask', reason: GATE_DENY_REASON };
|
|
90
94
|
return { action: 'deny', reason: GATE_DENY_REASON };
|
|
91
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* 从宿主的 inbox 负载里尽量捞出用户消息文本。
|
|
98
|
+
*
|
|
99
|
+
* 不同宿主版本的字段名不统一(text / content / parts),取不到时返回空串;
|
|
100
|
+
* 空串表示"不知道用户说了什么",此时保持旧行为(关门禁)最安全。
|
|
101
|
+
* @param payload `agent/inbox/inserted` 的事件负载。
|
|
102
|
+
*/
|
|
103
|
+
function extractMessageText(payload) {
|
|
104
|
+
const message = payload?.message;
|
|
105
|
+
if (typeof message === 'string')
|
|
106
|
+
return message.trim();
|
|
107
|
+
if (!message || typeof message !== 'object')
|
|
108
|
+
return '';
|
|
109
|
+
const record = message;
|
|
110
|
+
for (const key of ['text', 'content', 'body', 'prompt', 'parts']) {
|
|
111
|
+
const value = record[key];
|
|
112
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
113
|
+
return value.trim();
|
|
114
|
+
if (!Array.isArray(value))
|
|
115
|
+
continue;
|
|
116
|
+
const joined = value
|
|
117
|
+
.map((part) => (typeof part === 'string' ? part : part?.text))
|
|
118
|
+
.filter((part) => typeof part === 'string' && part.trim() !== '')
|
|
119
|
+
.join('\n');
|
|
120
|
+
if (joined.trim() !== '')
|
|
121
|
+
return joined.trim();
|
|
122
|
+
}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
92
125
|
/** 每个 agent 的门禁状态表(agent 级隔离)。 */
|
|
93
126
|
export class GateRegistry {
|
|
94
127
|
states = new Map();
|
|
@@ -144,6 +177,17 @@ export function registerGate(ctx, gates, options) {
|
|
|
144
177
|
ctx.logger?.info?.(`[grill-first] 拦截 ${exec.name}(需求未确认,agent=${agentId})`);
|
|
145
178
|
return { kind: 'deny', reason: decision.reason ?? GATE_DENY_REASON };
|
|
146
179
|
}
|
|
180
|
+
if (decision.action === 'ask') {
|
|
181
|
+
// 只问一次:卡片弹过之后无论用户点头还是否决,都由用户决定,插件不再逐次弹
|
|
182
|
+
// (否则一次任务里写 20 个文件就会弹 20 张卡片)。
|
|
183
|
+
ctx.logger?.info?.(`[grill-first] 询问 ${exec.name}(需求未确认,agent=${agentId})`);
|
|
184
|
+
gates.open(agentId);
|
|
185
|
+
return {
|
|
186
|
+
kind: 'ask',
|
|
187
|
+
reason: decision.reason ?? GATE_DENY_REASON,
|
|
188
|
+
displayReason: GATE_ASK_REASON,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
147
191
|
if (options.mode === 'warn' && state === 'closed' && isMutatingTool(exec?.name ?? '')) {
|
|
148
192
|
options.onWarning?.(exec.name, agentId);
|
|
149
193
|
}
|
|
@@ -159,6 +203,9 @@ export function registerGate(ctx, gates, options) {
|
|
|
159
203
|
const agentId = payload?.agent?.id ?? '';
|
|
160
204
|
if (agentId === '' || !isRootAgent(ctx, agentId))
|
|
161
205
|
return;
|
|
206
|
+
const text = extractMessageText(payload);
|
|
207
|
+
if (text !== '' && options.shouldResetOnMessage && !options.shouldResetOnMessage(text))
|
|
208
|
+
return;
|
|
162
209
|
gates.close(agentId);
|
|
163
210
|
});
|
|
164
211
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* 在插件卸载/配置变更重挂载时自动清理。
|
|
6
6
|
*/
|
|
7
7
|
import { normalizeConfig } from './config.js';
|
|
8
|
+
import { judgeTask } from './judge/heuristics.js';
|
|
8
9
|
import { Config } from './configSchema.js';
|
|
9
10
|
import { GateRegistry, registerGate } from './gate/preExecute.js';
|
|
10
11
|
import { PROTOCOL_SECTION_NAME, PROTOCOL_SECTION_ORDER, buildProtocolText } from './prompt/protocol.js';
|
|
@@ -68,6 +69,7 @@ export function apply(ctx, rawConfig) {
|
|
|
68
69
|
gates,
|
|
69
70
|
sessions,
|
|
70
71
|
workspaceRoot,
|
|
72
|
+
memoryScope: () => config.memoryScope,
|
|
71
73
|
...(ctx.logger ? { logger: ctx.logger } : {}),
|
|
72
74
|
}));
|
|
73
75
|
register(createGrillConfirmTool({
|
|
@@ -75,6 +77,7 @@ export function apply(ctx, rawConfig) {
|
|
|
75
77
|
gates,
|
|
76
78
|
sessions,
|
|
77
79
|
workspaceRoot,
|
|
80
|
+
memoryScope: () => config.memoryScope,
|
|
78
81
|
questions: () => ctx.get('userQuestions'),
|
|
79
82
|
...(ctx.logger ? { logger: ctx.logger } : {}),
|
|
80
83
|
}));
|
|
@@ -99,6 +102,16 @@ export function apply(ctx, rawConfig) {
|
|
|
99
102
|
installProtocolSection(ctx, config);
|
|
100
103
|
registerGate(ctx, gates, {
|
|
101
104
|
mode: config.gateMode,
|
|
105
|
+
// 新消息到达时先看一眼内容:追问/询问类不关门禁,否则"刚确认完又被拦"。
|
|
106
|
+
shouldResetOnMessage: (text) => {
|
|
107
|
+
const judged = judgeTask({
|
|
108
|
+
text,
|
|
109
|
+
mode: 'new-task',
|
|
110
|
+
bypassKeywords: config.bypassKeywords,
|
|
111
|
+
intensity: config.intensity,
|
|
112
|
+
});
|
|
113
|
+
return judged.verdict !== 'clear';
|
|
114
|
+
},
|
|
102
115
|
onWarning: (toolName, agentId) => {
|
|
103
116
|
ctx.logger?.warn(`[grill-first] 需求尚未确认(warn 模式,仅记录未拦截):${toolName}(agent=${agentId})`);
|
|
104
117
|
},
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 纯函数启发式判定(R1):不调 LLM,只看任务文本判断"需求是否已经够清楚"。
|
|
3
3
|
*
|
|
4
|
-
* 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 文本过短且无动作 →
|
|
4
|
+
* 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 纯询问(问句/只读咨询)→ 文本过短且无动作 →
|
|
5
5
|
* 四个需求维度(目标/范围/约束/验收标准)的覆盖度。
|
|
6
6
|
*/
|
|
7
7
|
/** 判定模式:新任务 / 任务中途的需求变更检查。 */
|
|
8
8
|
export type JudgeMode = 'new-task' | 'drift-check';
|
|
9
|
+
/** 追问力度:只影响判定阈值,不改变判定顺序。 */
|
|
10
|
+
export type JudgeIntensity = 'gentle' | 'standard' | 'grill';
|
|
9
11
|
/** 判定结论。 */
|
|
10
12
|
export type JudgeVerdict = 'clear' | 'needs-clarification';
|
|
11
13
|
/** 四个需求维度是否在文本里被提到。 */
|
|
@@ -20,6 +22,8 @@ export interface JudgeInput {
|
|
|
20
22
|
text: string;
|
|
21
23
|
mode: JudgeMode;
|
|
22
24
|
bypassKeywords?: readonly string[];
|
|
25
|
+
/** 追问力度(默认 standard,保持旧行为)。 */
|
|
26
|
+
intensity?: JudgeIntensity;
|
|
23
27
|
}
|
|
24
28
|
/** 判定输出。 */
|
|
25
29
|
export interface JudgeResult {
|
|
@@ -45,6 +49,13 @@ export declare function hasBypassKeyword(text: string, keywords: readonly string
|
|
|
45
49
|
export declare function isFollowUpMessage(text: string): boolean;
|
|
46
50
|
/** 由缺失维度生成追问提示。 */
|
|
47
51
|
export declare function buildFocusHints(hit: DimensionHit, extra?: string[]): string[];
|
|
52
|
+
/**
|
|
53
|
+
* 是否是"纯询问"消息:问句或让我查/看/评估,且没有任何动手信号。
|
|
54
|
+
*
|
|
55
|
+
* 这类消息不需要澄清——用户就是要一个回答,继续追问反而是打扰。
|
|
56
|
+
* @param text 用户原文。
|
|
57
|
+
*/
|
|
58
|
+
export declare function isInquiry(text: string): boolean;
|
|
48
59
|
/**
|
|
49
60
|
* 判定任务文本是否需要澄清(纯函数,可单测)。
|
|
50
61
|
* @param input 任务文本 + 模式 + 跳过关键词。
|
package/dist/judge/heuristics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 纯函数启发式判定(R1):不调 LLM,只看任务文本判断"需求是否已经够清楚"。
|
|
3
3
|
*
|
|
4
|
-
* 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 文本过短且无动作 →
|
|
4
|
+
* 判定顺序(先命中先返回):bypass 关键词 → 追问/继续类消息 → 纯询问(问句/只读咨询)→ 文本过短且无动作 →
|
|
5
5
|
* 四个需求维度(目标/范围/约束/验收标准)的覆盖度。
|
|
6
6
|
*/
|
|
7
7
|
/** 默认跳过关键词(R4)。 */
|
|
@@ -14,17 +14,23 @@ export const DIMENSION_HINTS = {
|
|
|
14
14
|
acceptance: '验收标准缺失:怎样算完成?用什么方式验证?',
|
|
15
15
|
};
|
|
16
16
|
/** 维度关键词:目标(有动作、有产出)。 */
|
|
17
|
-
const GOAL_WORDS = /(
|
|
17
|
+
const GOAL_WORDS = /(做|写|实现|搭建|开发|创建|新增|加个|修复|改|优化|重构|迁移|升级|翻译|整理|分析|生成|设计|部署|发布|测试|排查|调研|对比|总结|转换|导出|导入|接入|对接|编写|画|制作|搞|弄|\b(?:fix|add|implement|build|create|refactor|migrate|write|update|remove|delete|optimi[sz]e|translate|generate|analy[sz]e|design|deploy|publish|rewrite|upgrade|install|configure|support|enable|disable|clean|split|merge|rename)\b)/i;
|
|
18
18
|
/** 维度关键词:范围(边界、对象)。 */
|
|
19
|
-
const SCOPE_WORDS = /(
|
|
19
|
+
const SCOPE_WORDS = /(只|仅|只要|只需要|范围|限定|针对|面向|所有|全部|部分|哪些|哪个|模块|页面|文件|目录|接口|字段|表|库|版本|第一步|先做|不含|除外|\b(?:only|all|each|every|field|fields|page|pages|file|files|directory|module|api|endpoint|component|table|schema|version|repo|repository|branch|config|first)\b)/i;
|
|
20
20
|
/** 维度关键词:约束(技术栈、限制、风格)。忽略大小写,便于匹配 Python / UTF-8 这类写法。 */
|
|
21
|
-
const CONSTRAINT_WORDS = /(用|基于|必须|不能|不要|需要|依赖|兼容|保持|风格|注释|技术栈|框架|语言|不许|禁止|限制|预算|时间|截止|格式|规范|utf-?8|中文|英文|node|python|js|ts|react|vue|java|go|rust)/i;
|
|
21
|
+
const CONSTRAINT_WORDS = /(用|基于|必须|不能|不要|需要|依赖|兼容|保持|风格|注释|技术栈|框架|语言|不许|禁止|限制|预算|时间|截止|格式|规范|utf-?8|中文|英文|node|python|js|ts|react|vue|java|go|rust|\b(?:must|should|shall|need|needs|require|requires|required|use|using|based\s+on|compatible|compatibility|keep|preserve|avoid|without|existing)\b)/i;
|
|
22
22
|
/** 维度关键词:验收标准。 */
|
|
23
|
-
const ACCEPTANCE_WORDS = /(
|
|
23
|
+
const ACCEPTANCE_WORDS = /(验收|标准|测试|通过|算完成|完成标准|结果|输出|期望|应该|返回|效果|达到|满足|可运行|跑通|全绿|正确|不报错|可用|验证|检查|示例|样例|截图|对比|\b(?:acceptance|criteria|test|tests|pass|passes|passing|expect|expected|output|verify|verified|green|works|working|screenshot|sample|example)\b)/i;
|
|
24
24
|
/** 动作动词(用于"短文本是否已构成一个可执行指令")。 */
|
|
25
25
|
const ACTION_VERB = /(做|写|改|加|删|实现|搭建|开发|修复|优化|翻译|整理|分析|生成|设计|部署|测试|排查|调研|总结|转换|导出|接入|创建|重构|迁移|升级|安装|配置|画|制作)/;
|
|
26
26
|
/** 追问/继续类消息的开头。 */
|
|
27
27
|
const FOLLOW_UP_LEAD = /^(继续|接着|然后呢|然后|下一步|再来|嗯|好|可以|行|ok|OK|对|是的|收到|谢谢|辛苦)/;
|
|
28
|
+
/** 问句收尾:出现即说明用户在提问,而不是在派活。 */
|
|
29
|
+
const QUESTION_TAIL = /(吗|呢|么|嘛|是不是|能不能|可不可以|有没有|怎么样|如何|为什么|为啥|啥意思|多少|哪个好)\s*[??]?$|[??]\s*$/;
|
|
30
|
+
/** 询问式开头:让我查/看/评估/解释,而不是让我动手。 */
|
|
31
|
+
const INQUIRY_LEAD = /^(?:please\s+|pls\s+|could you\s+|can you\s+)?(请问|问一下|想问|问下|看看|看一下|检查|查一下|查查|列出|列一下|统计|解释|说明|对比|评估|推荐|你觉得|你认为|这个|那个|现在|目前|为啥|为什么|look|check|see|show|list|tell|explain|describe|review|evaluate|summari[sz]e|search|find|verify|what|why|how|which)/i;
|
|
32
|
+
/** 明确的动手信号:一旦出现就不算纯询问("改一下这个吗"仍然要先澄清)。 */
|
|
33
|
+
const WRITE_SIGNAL = /(改|写|删|新建|实现|修复|重构|迁移|生成|替换|安装|部署|发布|合并|提交|制作|画|创建|搭建|开发|添加|增加|移除)|\b(?:fix|add|implement|build|create|refactor|migrate|write|update|remove|delete|change|edit|rename|move|split|merge|install|deploy|publish|upgrade|generate|replace)\b/i;
|
|
28
34
|
/** 具体化信号:出现即说明用户给了可落地的细节。 */
|
|
29
35
|
const SPECIFIC_PATTERNS = [
|
|
30
36
|
/[A-Za-z0-9_\-./\\]+\.(ts|js|mjs|cjs|tsx|jsx|json|md|py|go|rs|java|c|cc|cpp|h|hpp|yml|yaml|toml|txt|csv|xlsx|png|jpg|jpeg|svg|html|css|vue|sh|ps1)/i,
|
|
@@ -92,6 +98,26 @@ export function buildFocusHints(hit, extra = []) {
|
|
|
92
98
|
}
|
|
93
99
|
return [...extra, ...hints].slice(0, 4);
|
|
94
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* 是否是"纯询问"消息:问句或让我查/看/评估,且没有任何动手信号。
|
|
103
|
+
*
|
|
104
|
+
* 这类消息不需要澄清——用户就是要一个回答,继续追问反而是打扰。
|
|
105
|
+
* @param text 用户原文。
|
|
106
|
+
*/
|
|
107
|
+
export function isInquiry(text) {
|
|
108
|
+
const flat = text.trim();
|
|
109
|
+
if (flat === '')
|
|
110
|
+
return false;
|
|
111
|
+
if (WRITE_SIGNAL.test(flat))
|
|
112
|
+
return false;
|
|
113
|
+
return QUESTION_TAIL.test(flat) || INQUIRY_LEAD.test(flat);
|
|
114
|
+
}
|
|
115
|
+
/** 各力度档位的判定阈值:维度下限 + 是否强制覆盖验收标准。 */
|
|
116
|
+
const INTENSITY_THRESHOLDS = {
|
|
117
|
+
gentle: { dims: 2, needAcceptance: false },
|
|
118
|
+
standard: { dims: 3, needAcceptance: false },
|
|
119
|
+
grill: { dims: 3, needAcceptance: true },
|
|
120
|
+
};
|
|
95
121
|
/**
|
|
96
122
|
* 判定任务文本是否需要澄清(纯函数,可单测)。
|
|
97
123
|
* @param input 任务文本 + 模式 + 跳过关键词。
|
|
@@ -102,6 +128,8 @@ export function judgeTask(input) {
|
|
|
102
128
|
const dimensions = detectDimensions(text);
|
|
103
129
|
const satisfied = satisfiedCount(dimensions);
|
|
104
130
|
const score = satisfied * 25;
|
|
131
|
+
const intensity = input.intensity ?? 'standard';
|
|
132
|
+
const threshold = INTENSITY_THRESHOLDS[intensity] ?? INTENSITY_THRESHOLDS.standard;
|
|
105
133
|
if (text === '') {
|
|
106
134
|
return {
|
|
107
135
|
verdict: 'needs-clarification',
|
|
@@ -117,9 +145,12 @@ export function judgeTask(input) {
|
|
|
117
145
|
if (isFollowUpMessage(text)) {
|
|
118
146
|
return { verdict: 'clear', score: 95, focus_hints: [], dimensions, reason: 'follow-up' };
|
|
119
147
|
}
|
|
148
|
+
if (isInquiry(text)) {
|
|
149
|
+
return { verdict: 'clear', score: 90, focus_hints: [], dimensions, reason: 'inquiry' };
|
|
150
|
+
}
|
|
120
151
|
if (input.mode === 'drift-check') {
|
|
121
152
|
const drift = DRIFT_MARKERS.test(text);
|
|
122
|
-
if (satisfied >=
|
|
153
|
+
if (satisfied >= threshold.dims && text.length >= 20) {
|
|
123
154
|
return { verdict: 'clear', score, focus_hints: [], dimensions, reason: 'drift-well-specified' };
|
|
124
155
|
}
|
|
125
156
|
return {
|
|
@@ -141,7 +172,7 @@ export function judgeTask(input) {
|
|
|
141
172
|
reason: 'too-short',
|
|
142
173
|
};
|
|
143
174
|
}
|
|
144
|
-
if (satisfied >=
|
|
175
|
+
if (satisfied >= threshold.dims && (!threshold.needAcceptance || dimensions.acceptance)) {
|
|
145
176
|
return { verdict: 'clear', score, focus_hints: [], dimensions, reason: 'dimensions-covered' };
|
|
146
177
|
}
|
|
147
178
|
if (satisfied === 2 && (hasSpecifics(text) || TRANSFORM_PATTERN.test(text))) {
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface MemorySnapshot {
|
|
|
26
26
|
}
|
|
27
27
|
/** forget() 的作用范围。 */
|
|
28
28
|
export type MemoryScope = 'global' | 'project' | 'all';
|
|
29
|
+
/** 项目层记忆的落点(0.1.4):跟着工作区走,或全局共享一份。 */
|
|
30
|
+
export type ProjectScope = 'project' | 'global';
|
|
31
|
+
/** 项目层历史条目的保留上限(0.1.4 修:以前只增不减,文件会越长越大)。 */
|
|
32
|
+
export declare const MAX_HISTORY = 100;
|
|
29
33
|
/** 摘要长度上限(字符数),供注入提示用。 */
|
|
30
34
|
export declare const SUMMARY_LIMIT = 500;
|
|
31
35
|
/** 空全局层。 */
|
|
@@ -44,13 +48,18 @@ export declare function resolveWorkspace(workspaceRoot?: string): string;
|
|
|
44
48
|
* 固定到 `~/.dsh/grill-first/`(跨 profile 共享用户偏好,语义上也更合理)。
|
|
45
49
|
*/
|
|
46
50
|
export declare function globalMemoryPath(): string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 项目记忆文件路径。
|
|
53
|
+
* @param workspaceRoot 工作区根目录。
|
|
54
|
+
* @param scope project(默认)= `<workspace>/.dsh/grill-first/memory.json`;
|
|
55
|
+
* global = 与 global.json 同目录的 `project.json`(不想往项目里写文件时用)。
|
|
56
|
+
*/
|
|
57
|
+
export declare function projectMemoryPath(workspaceRoot?: string, scope?: ProjectScope): string;
|
|
49
58
|
/**
|
|
50
59
|
* 读取两层记忆。
|
|
51
60
|
* @param workspaceRoot 工作区根目录;省略时用 `process.cwd()`。
|
|
52
61
|
*/
|
|
53
|
-
export declare function loadMemory(workspaceRoot?: string): Promise<MemorySnapshot>;
|
|
62
|
+
export declare function loadMemory(workspaceRoot?: string, scope?: ProjectScope): Promise<MemorySnapshot>;
|
|
54
63
|
/**
|
|
55
64
|
* 追加一条全局偏好(按文本去重,重复则刷新 updated 时间)。
|
|
56
65
|
* @param text 偏好原文,例如"注释一律用中文"。
|
|
@@ -63,14 +72,14 @@ export declare function savePreference(text: string, tags: string[], workspaceRo
|
|
|
63
72
|
* @param text 项目背景原文。
|
|
64
73
|
* @param workspaceRoot 工作区根目录。
|
|
65
74
|
*/
|
|
66
|
-
export declare function saveProjectContext(text: string, workspaceRoot?: string): Promise<void>;
|
|
75
|
+
export declare function saveProjectContext(text: string, workspaceRoot?: string, scope?: ProjectScope): Promise<void>;
|
|
67
76
|
/**
|
|
68
77
|
* 追加一条历史需求记录(R3:需求清单只进这里,绝不落盘成需求文件)。
|
|
69
78
|
* @param task 任务原文或摘要。
|
|
70
79
|
* @param requirements 用户确认过的四段式需求清单。
|
|
71
80
|
* @param workspaceRoot 工作区根目录。
|
|
72
81
|
*/
|
|
73
|
-
export declare function saveHistory(task: string, requirements: string, workspaceRoot?: string): Promise<void>;
|
|
82
|
+
export declare function saveHistory(task: string, requirements: string, workspaceRoot?: string, scope?: ProjectScope): Promise<void>;
|
|
74
83
|
/**
|
|
75
84
|
* 清除记忆。
|
|
76
85
|
* @param scope `global` 只清全局层、`project` 只清项目层、`all` 两层都清。
|
package/dist/memory/store.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
/** 项目层历史条目的保留上限(0.1.4 修:以前只增不减,文件会越长越大)。 */
|
|
12
|
+
export const MAX_HISTORY = 100;
|
|
11
13
|
/** 摘要长度上限(字符数),供注入提示用。 */
|
|
12
14
|
export const SUMMARY_LIMIT = 500;
|
|
13
15
|
/** 摘要里各段的条数上限。 */
|
|
@@ -43,8 +45,15 @@ export function globalMemoryPath() {
|
|
|
43
45
|
const base = override && override.trim() !== '' ? path.resolve(override) : path.join(homedir(), '.dsh', 'grill-first');
|
|
44
46
|
return path.join(base, 'global.json');
|
|
45
47
|
}
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* 项目记忆文件路径。
|
|
50
|
+
* @param workspaceRoot 工作区根目录。
|
|
51
|
+
* @param scope project(默认)= `<workspace>/.dsh/grill-first/memory.json`;
|
|
52
|
+
* global = 与 global.json 同目录的 `project.json`(不想往项目里写文件时用)。
|
|
53
|
+
*/
|
|
54
|
+
export function projectMemoryPath(workspaceRoot, scope = 'project') {
|
|
55
|
+
if (scope === 'global')
|
|
56
|
+
return path.join(path.dirname(globalMemoryPath()), 'project.json');
|
|
48
57
|
return path.join(resolveWorkspace(workspaceRoot), '.dsh', 'grill-first', 'memory.json');
|
|
49
58
|
}
|
|
50
59
|
/** 归一化用于去重比较的文本(去空白、统一小写、去尾部标点)。 */
|
|
@@ -109,9 +118,9 @@ function coerceProject(value) {
|
|
|
109
118
|
* 读取两层记忆。
|
|
110
119
|
* @param workspaceRoot 工作区根目录;省略时用 `process.cwd()`。
|
|
111
120
|
*/
|
|
112
|
-
export async function loadMemory(workspaceRoot) {
|
|
121
|
+
export async function loadMemory(workspaceRoot, scope = 'project') {
|
|
113
122
|
const global = coerceGlobal(await readJson(globalMemoryPath(), emptyGlobal()));
|
|
114
|
-
const project = coerceProject(await readJson(projectMemoryPath(workspaceRoot), emptyProject()));
|
|
123
|
+
const project = coerceProject(await readJson(projectMemoryPath(workspaceRoot, scope), emptyProject()));
|
|
115
124
|
return { global, project };
|
|
116
125
|
}
|
|
117
126
|
/**
|
|
@@ -152,11 +161,11 @@ export async function savePreference(text, tags, workspaceRoot) {
|
|
|
152
161
|
* @param text 项目背景原文。
|
|
153
162
|
* @param workspaceRoot 工作区根目录。
|
|
154
163
|
*/
|
|
155
|
-
export async function saveProjectContext(text, workspaceRoot) {
|
|
164
|
+
export async function saveProjectContext(text, workspaceRoot, scope = 'project') {
|
|
156
165
|
const trimmed = text.trim();
|
|
157
166
|
if (trimmed === '')
|
|
158
167
|
return;
|
|
159
|
-
const file = projectMemoryPath(workspaceRoot);
|
|
168
|
+
const file = projectMemoryPath(workspaceRoot, scope);
|
|
160
169
|
const memory = coerceProject(await readJson(file, emptyProject()));
|
|
161
170
|
const key = normalizeKey(trimmed);
|
|
162
171
|
if (!memory.project_context.some((t) => normalizeKey(t) === key)) {
|
|
@@ -170,14 +179,17 @@ export async function saveProjectContext(text, workspaceRoot) {
|
|
|
170
179
|
* @param requirements 用户确认过的四段式需求清单。
|
|
171
180
|
* @param workspaceRoot 工作区根目录。
|
|
172
181
|
*/
|
|
173
|
-
export async function saveHistory(task, requirements, workspaceRoot) {
|
|
174
|
-
const file = projectMemoryPath(workspaceRoot);
|
|
182
|
+
export async function saveHistory(task, requirements, workspaceRoot, scope = 'project') {
|
|
183
|
+
const file = projectMemoryPath(workspaceRoot, scope);
|
|
175
184
|
const memory = coerceProject(await readJson(file, emptyProject()));
|
|
176
185
|
memory.history.push({
|
|
177
186
|
task: task.trim().slice(0, 200),
|
|
178
187
|
requirements: requirements.trim().slice(0, 2000),
|
|
179
188
|
date: new Date().toISOString().slice(0, 10),
|
|
180
189
|
});
|
|
190
|
+
// 只保留最近 MAX_HISTORY 条:摘要只用最后 3 条,其余留着也读不到。
|
|
191
|
+
if (memory.history.length > MAX_HISTORY)
|
|
192
|
+
memory.history = memory.history.slice(-MAX_HISTORY);
|
|
181
193
|
await writeJsonAtomic(file, memory);
|
|
182
194
|
}
|
|
183
195
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* R3 红线:需求清单只进记忆的 history 字段,绝不写任何需求文件。
|
|
11
11
|
*/
|
|
12
|
-
import type { GrillConfig } from '../config.js';
|
|
12
|
+
import type { GrillConfig, MemoryScope } from '../config.js';
|
|
13
13
|
import type { GateRegistry } from '../gate/preExecute.js';
|
|
14
14
|
import type { GrillAgent, ToolDefinition, UserQuestionsService } from '../types.js';
|
|
15
15
|
import type { SessionRegistry } from './sessions.js';
|
|
@@ -20,6 +20,8 @@ export interface GrillConfirmDeps {
|
|
|
20
20
|
sessions: SessionRegistry;
|
|
21
21
|
/** 解析当前会话的工作区根目录(项目层记忆落点)。 */
|
|
22
22
|
workspaceRoot: (agent?: GrillAgent) => string;
|
|
23
|
+
/** 项目层记忆落在工作区还是插件家目录(memoryScope 配置)。 */
|
|
24
|
+
memoryScope?: () => MemoryScope;
|
|
23
25
|
/** 结构化提问服务(可能不存在)。 */
|
|
24
26
|
questions: () => UserQuestionsService | undefined;
|
|
25
27
|
logger?: {
|
|
@@ -38,6 +40,9 @@ export interface GrillConfirmResult {
|
|
|
38
40
|
skipped?: boolean;
|
|
39
41
|
reason?: string;
|
|
40
42
|
error?: string;
|
|
43
|
+
/** 已追问轮数 / 剩余轮数(选"要改"时给出,供模型判断还要不要继续问)。 */
|
|
44
|
+
rounds_used?: number;
|
|
45
|
+
rounds_left?: number;
|
|
41
46
|
saved?: {
|
|
42
47
|
history: boolean;
|
|
43
48
|
preferences: number;
|
|
@@ -104,14 +104,15 @@ export function createGrillConfirmTool(deps) {
|
|
|
104
104
|
if (config.memoryEnabled) {
|
|
105
105
|
const root = deps.workspaceRoot(exec?.agent);
|
|
106
106
|
try {
|
|
107
|
-
|
|
107
|
+
const scope = deps.memoryScope?.() ?? 'project';
|
|
108
|
+
await saveHistory(task, requirements, root, scope);
|
|
108
109
|
saved.history = true;
|
|
109
110
|
for (const preference of preferences) {
|
|
110
111
|
await savePreference(preference, ['用户确认'], root);
|
|
111
112
|
saved.preferences += 1;
|
|
112
113
|
}
|
|
113
114
|
const context = requirements.replace(/\s+/g, ' ').slice(0, PROJECT_CONTEXT_LIMIT);
|
|
114
|
-
await saveProjectContext(context, root);
|
|
115
|
+
await saveProjectContext(context, root, scope);
|
|
115
116
|
saved.project_context = true;
|
|
116
117
|
}
|
|
117
118
|
catch (error) {
|
|
@@ -176,12 +177,22 @@ export function createGrillConfirmTool(deps) {
|
|
|
176
177
|
const result = await accept('structured-question');
|
|
177
178
|
return custom === '' ? result : { ...result, user_note: custom };
|
|
178
179
|
}
|
|
180
|
+
const rounds = deps.sessions.bumpRound(agentId);
|
|
181
|
+
const maxRounds = config.maxRounds;
|
|
182
|
+
const roundsLeft = Math.max(0, maxRounds - rounds);
|
|
179
183
|
return {
|
|
180
184
|
confirmed: false,
|
|
181
185
|
requirements,
|
|
182
186
|
feedback: custom !== ''
|
|
183
187
|
? custom
|
|
184
188
|
: '用户选择了"要改",但没有写明改哪里:请追问具体修改点,修订需求清单后再次调用 grill_confirm。',
|
|
189
|
+
rounds_used: rounds,
|
|
190
|
+
rounds_left: roundsLeft,
|
|
191
|
+
...(roundsLeft === 0
|
|
192
|
+
? {
|
|
193
|
+
instructions: `已达追问轮数上限(${maxRounds} 轮):不要再问,直接按当前理解开工——下次调用 grill_start 会返回 clear。`,
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
185
196
|
};
|
|
186
197
|
},
|
|
187
198
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - clear:放行(门禁置 open),模型直接开工;
|
|
5
5
|
* - needs-clarification:门禁置 closed,并返回记忆摘要、追问方向与轮次上限。
|
|
6
6
|
*/
|
|
7
|
-
import type { GrillConfig } from '../config.js';
|
|
7
|
+
import type { GrillConfig, MemoryScope } from '../config.js';
|
|
8
8
|
import type { GateRegistry } from '../gate/preExecute.js';
|
|
9
9
|
import type { GrillAgent, ToolDefinition } from '../types.js';
|
|
10
10
|
import type { SessionRegistry } from './sessions.js';
|
|
@@ -15,6 +15,8 @@ export interface GrillStartDeps {
|
|
|
15
15
|
sessions: SessionRegistry;
|
|
16
16
|
/** 解析当前会话的工作区根目录(项目层记忆落点)。 */
|
|
17
17
|
workspaceRoot: (agent?: GrillAgent) => string;
|
|
18
|
+
/** 项目层记忆落在工作区还是插件家目录(memoryScope 配置)。 */
|
|
19
|
+
memoryScope?: () => MemoryScope;
|
|
18
20
|
logger?: {
|
|
19
21
|
warn(message: string): void;
|
|
20
22
|
};
|
|
@@ -35,6 +37,8 @@ export interface GrillStartResult {
|
|
|
35
37
|
task_id?: string;
|
|
36
38
|
previous_task?: string;
|
|
37
39
|
hint?: string;
|
|
40
|
+
/** 已经追问过的轮数(达到 maxRounds 后不再追问)。 */
|
|
41
|
+
rounds_used?: number;
|
|
38
42
|
/** 当前运行的插件版本(取自 package.json):用于确认改动是否已随重启生效。 */
|
|
39
43
|
version?: string;
|
|
40
44
|
}
|
package/dist/tools/grillStart.js
CHANGED
|
@@ -43,6 +43,7 @@ const OUTPUT_SCHEMA = {
|
|
|
43
43
|
task_id: { type: 'string' },
|
|
44
44
|
previous_task: { type: 'string' },
|
|
45
45
|
hint: { type: 'string' },
|
|
46
|
+
rounds_used: { type: 'number', description: '已经追问过的轮数' },
|
|
46
47
|
version: { type: 'string', description: '运行中的插件版本;若缺失说明宿主加载的是旧版代码,需重启 DSH' },
|
|
47
48
|
},
|
|
48
49
|
additionalProperties: true,
|
|
@@ -77,7 +78,12 @@ export function createGrillStartTool(deps) {
|
|
|
77
78
|
version: PLUGIN_VERSION,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
|
-
const judged = judgeTask({
|
|
81
|
+
const judged = judgeTask({
|
|
82
|
+
text: task,
|
|
83
|
+
mode,
|
|
84
|
+
bypassKeywords: config.bypassKeywords,
|
|
85
|
+
intensity: config.intensity,
|
|
86
|
+
});
|
|
81
87
|
const previous = deps.sessions.get(agentId);
|
|
82
88
|
if (judged.verdict === 'clear') {
|
|
83
89
|
deps.sessions.end(agentId);
|
|
@@ -93,12 +99,29 @@ export function createGrillStartTool(deps) {
|
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
const startedAt = Date.now();
|
|
96
|
-
|
|
102
|
+
// maxRounds 兜底:问够轮数就不再问,直接按当前理解开工(0.1.4 修:以前这个配置项根本没生效)。
|
|
103
|
+
const roundsUsed = previous?.rounds ?? 0;
|
|
104
|
+
if (roundsUsed >= config.maxRounds) {
|
|
105
|
+
deps.sessions.end(agentId);
|
|
106
|
+
deps.gates.open(agentId);
|
|
107
|
+
return {
|
|
108
|
+
verdict: 'clear',
|
|
109
|
+
skip: true,
|
|
110
|
+
reason: 'max-rounds-reached',
|
|
111
|
+
score: judged.score,
|
|
112
|
+
gate_state: 'open',
|
|
113
|
+
mode,
|
|
114
|
+
rounds_used: roundsUsed,
|
|
115
|
+
hint: `已经追问 ${roundsUsed} 轮(上限 ${config.maxRounds}),按当前理解直接开工;有歧义就先做最小可验证版本。`,
|
|
116
|
+
version: PLUGIN_VERSION,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
deps.sessions.start({ agentId, task, mode, startedAt, rounds: roundsUsed });
|
|
97
120
|
deps.gates.close(agentId);
|
|
98
121
|
let memoryContext = '';
|
|
99
122
|
if (config.memoryEnabled) {
|
|
100
123
|
try {
|
|
101
|
-
memoryContext = summarize(await loadMemory(deps.workspaceRoot(exec?.agent)));
|
|
124
|
+
memoryContext = summarize(await loadMemory(deps.workspaceRoot(exec?.agent), (deps.memoryScope?.() ?? 'project')));
|
|
102
125
|
}
|
|
103
126
|
catch (error) {
|
|
104
127
|
deps.logger?.warn(`[grill-first] 读取记忆失败:${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -118,6 +141,7 @@ export function createGrillStartTool(deps) {
|
|
|
118
141
|
intensity: config.intensity,
|
|
119
142
|
mode,
|
|
120
143
|
task_id: `${agentId}:${startedAt}`,
|
|
144
|
+
rounds_used: roundsUsed,
|
|
121
145
|
version: PLUGIN_VERSION,
|
|
122
146
|
...(previous?.task ? { previous_task: previous.task } : {}),
|
|
123
147
|
};
|
package/dist/tools/sessions.d.ts
CHANGED
|
@@ -9,12 +9,16 @@ export interface GrillSession {
|
|
|
9
9
|
task: string;
|
|
10
10
|
mode: JudgeMode;
|
|
11
11
|
startedAt: number;
|
|
12
|
+
/** 已经追问过几轮(用户选"要改"算一轮),用于落实 maxRounds 上限。 */
|
|
13
|
+
rounds?: number;
|
|
12
14
|
}
|
|
13
15
|
/** agent → 会话 的内存态映射(进程内,不持久化)。 */
|
|
14
16
|
export declare class SessionRegistry {
|
|
15
17
|
private readonly sessions;
|
|
16
18
|
/** 登记/覆盖一个 agent 的当前任务。 */
|
|
17
19
|
start(session: GrillSession): void;
|
|
20
|
+
/** 记一轮追问(用户选"要改"时调用),返回累计轮数。 */
|
|
21
|
+
bumpRound(agentId: string): number;
|
|
18
22
|
/** 读取某个 agent 的当前任务。 */
|
|
19
23
|
get(agentId: string): GrillSession | undefined;
|
|
20
24
|
/** 结束某个 agent 的澄清会话(确认完成或跳过)。 */
|
package/dist/tools/sessions.js
CHANGED
|
@@ -5,7 +5,15 @@ export class SessionRegistry {
|
|
|
5
5
|
start(session) {
|
|
6
6
|
if (!session.agentId)
|
|
7
7
|
return;
|
|
8
|
-
this.sessions.set(session.agentId, session);
|
|
8
|
+
this.sessions.set(session.agentId, { ...session, rounds: session.rounds ?? 0 });
|
|
9
|
+
}
|
|
10
|
+
/** 记一轮追问(用户选"要改"时调用),返回累计轮数。 */
|
|
11
|
+
bumpRound(agentId) {
|
|
12
|
+
const session = this.sessions.get(agentId);
|
|
13
|
+
if (!session)
|
|
14
|
+
return 0;
|
|
15
|
+
session.rounds = (session.rounds ?? 0) + 1;
|
|
16
|
+
return session.rounds;
|
|
9
17
|
}
|
|
10
18
|
/** 读取某个 agent 的当前任务。 */
|
|
11
19
|
get(agentId) {
|