minimal-agent 0.18.10 → 0.18.11
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/package.json +1 -1
- package/src/advisor/advisorCoordinator.js +30 -6
- package/src/advisor/advisorCoordinator.ts +23 -6
- package/src/advisor/deliveryReview.js +23 -0
- package/src/advisor/deliveryReview.ts +22 -0
- package/src/advisor/presentation.js +3 -1
- package/src/advisor/presentation.ts +4 -1
- package/src/loop/README.md +44 -0
- package/src/loop/deliveryIntent.js +22 -0
- package/src/loop/deliveryIntent.ts +39 -0
- package/src/loop/messages.js +146 -0
- package/src/loop/messages.ts +166 -0
- package/src/loop/options.js +1 -0
- package/src/loop/options.ts +155 -0
- package/src/loop/progressStream.js +40 -0
- package/src/loop/progressStream.ts +44 -0
- package/src/loop/publication.js +94 -0
- package/src/loop/publication.ts +124 -0
- package/src/loop/requestPreparation.js +180 -0
- package/src/loop/requestPreparation.ts +226 -0
- package/src/loop/toolBatch.js +99 -0
- package/src/loop/toolBatch.ts +123 -0
- package/src/loop/toolExecutor.js +304 -0
- package/src/loop/toolExecutor.ts +363 -0
- package/src/loop/toolPresentation.js +135 -0
- package/src/loop/toolPresentation.ts +134 -0
- package/src/loop/toolSemantics.js +45 -0
- package/src/loop/toolSemantics.ts +51 -0
- package/src/loop.js +368 -1113
- package/src/loop.ts +480 -1380
- package/src/tools/advisor/advisor.js +5 -2
- package/src/tools/advisor/advisor.ts +6 -2
- package/src/tools/bash/semantics.js +33 -1
- package/src/tools/bash/semantics.ts +27 -1
- package/src/types.ts +5 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** 给 UI 展示用的参数预览(只取前 60 字符) */
|
|
2
|
+
export function previewArgs(rawJson) {
|
|
3
|
+
const oneLine = rawJson.replace(/\s+/g, ' ').trim();
|
|
4
|
+
if (oneLine.length <= 60)
|
|
5
|
+
return oneLine;
|
|
6
|
+
return oneLine.slice(0, 60) + '...';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 把工具名 + raw arguments JSON 转换成人话描述。
|
|
10
|
+
* UI 优先用这个显示在 spinner 行(如 "Reading src/foo.ts")。
|
|
11
|
+
* 已知工具走 switch;未知工具或 JSON parse 失败 fallback 到 `${toolName}(...)`。
|
|
12
|
+
*/
|
|
13
|
+
export function friendlyToolDescription(toolName, rawArgsJson, fileLabel) {
|
|
14
|
+
let args = {};
|
|
15
|
+
try {
|
|
16
|
+
args = JSON.parse(rawArgsJson) ?? {};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return `${toolName}(...)`;
|
|
20
|
+
}
|
|
21
|
+
const truncate = (s, n) => {
|
|
22
|
+
if (typeof s !== 'string')
|
|
23
|
+
return '';
|
|
24
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
25
|
+
};
|
|
26
|
+
const file = (value) => {
|
|
27
|
+
if (typeof value !== 'string')
|
|
28
|
+
return '';
|
|
29
|
+
if (fileLabel)
|
|
30
|
+
return fileLabel(value);
|
|
31
|
+
return value.length > 64 ? `…${value.slice(-63)}` : value;
|
|
32
|
+
};
|
|
33
|
+
switch (toolName) {
|
|
34
|
+
case 'Skill':
|
|
35
|
+
return `技能 · ${truncate(args.name, 60)}`;
|
|
36
|
+
case 'SearchSkills':
|
|
37
|
+
return `查找技能 · ${truncate(args.query, 60)}`;
|
|
38
|
+
case 'Advisor': {
|
|
39
|
+
const reasons = {
|
|
40
|
+
plan_review: '执行前评审',
|
|
41
|
+
delivery_review: '交付前核验',
|
|
42
|
+
stalled: '分析重复失败',
|
|
43
|
+
uncertainty: '核查不确定问题',
|
|
44
|
+
user_dissatisfaction: '复核用户反馈',
|
|
45
|
+
evidence_conflict: '核查证据冲突',
|
|
46
|
+
};
|
|
47
|
+
return `咨询顾问 · ${reasons[String(args.reason)] ?? '独立第二意见'}`;
|
|
48
|
+
}
|
|
49
|
+
case 'AdvisorDecision':
|
|
50
|
+
return `顾问建议 · ${truncate(args.decision ?? args.action, 40) || '记录核验与采纳结论'}`;
|
|
51
|
+
case 'AskUserQuestion':
|
|
52
|
+
return '询问关键决策';
|
|
53
|
+
case 'PresentResult':
|
|
54
|
+
return '提交成果与验证结果';
|
|
55
|
+
case 'PowerShell':
|
|
56
|
+
return `运行 PowerShell · ${truncate(args.description ?? args.command, 60)}`;
|
|
57
|
+
case 'OptimizeConversation':
|
|
58
|
+
return '优化对话';
|
|
59
|
+
case 'Read':
|
|
60
|
+
return `Reading ${file(args.file_path)}`;
|
|
61
|
+
case 'Edit':
|
|
62
|
+
return `Editing ${file(args.file_path)}`;
|
|
63
|
+
case 'MultiEdit': {
|
|
64
|
+
const n = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
65
|
+
return `Editing ${file(args.file_path)}${n ? ` (${n} 处)` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
case 'Write':
|
|
68
|
+
return `Writing ${file(args.file_path)}`;
|
|
69
|
+
case 'Trash':
|
|
70
|
+
return `Moving to Trash ${file(args.file_path ?? args.path)}`;
|
|
71
|
+
case 'ApplyPatch': {
|
|
72
|
+
const patch = typeof args.patch === 'string' ? args.patch : typeof args.input === 'string' ? args.input : '';
|
|
73
|
+
const files = [...patch.matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map((m) => `${file(m[2])}(${{ Add: '新增', Update: '修改', Delete: '删除' }[m[1]]})`);
|
|
74
|
+
if (!files.length)
|
|
75
|
+
files.push(...[...patch.matchAll(/^\+\+\+ [ab]\/([^\n]+)$/gm)].map((m) => file(m[1])));
|
|
76
|
+
return files.length > 0 ? `Editing ${files.join(', ')}` : 'Editing patch';
|
|
77
|
+
}
|
|
78
|
+
case 'NotebookEdit':
|
|
79
|
+
return `Editing ${file(args.notebook_path ?? args.file_path)}`;
|
|
80
|
+
case 'Bash':
|
|
81
|
+
return typeof args.description === 'string' && args.description.trim()
|
|
82
|
+
? `运行 ${truncate(args.description, 70)}`
|
|
83
|
+
: `Running \`${truncate(args.command, 40)}\``;
|
|
84
|
+
case 'Grep': {
|
|
85
|
+
const p = truncate(args.pattern, 30);
|
|
86
|
+
const g = typeof args.glob === 'string' && args.glob.length > 0 ? ` in ${truncate(args.glob, 25)}` : '';
|
|
87
|
+
return `Searching \`${p}\`${g}`;
|
|
88
|
+
}
|
|
89
|
+
case 'Glob':
|
|
90
|
+
return `Globbing \`${truncate(args.pattern, 50)}\``;
|
|
91
|
+
case 'WebBrowser': {
|
|
92
|
+
const actions = {
|
|
93
|
+
start: '启动',
|
|
94
|
+
navigate: '打开页面',
|
|
95
|
+
screenshot: '截图',
|
|
96
|
+
snapshot: '读取页面',
|
|
97
|
+
click: '点击',
|
|
98
|
+
fill: '填写',
|
|
99
|
+
evaluate: '执行页面脚本',
|
|
100
|
+
close: '关闭',
|
|
101
|
+
stop: '停止',
|
|
102
|
+
console: '查看控制台',
|
|
103
|
+
network: '查看网络请求',
|
|
104
|
+
};
|
|
105
|
+
let target = '';
|
|
106
|
+
try {
|
|
107
|
+
target = new URL(String(args.url)).host;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* no URL for local actions */
|
|
111
|
+
}
|
|
112
|
+
return `浏览器 · ${actions[String(args.action)] ?? (truncate(args.action, 32) || '操作页面')}${target ? ` · ${target}` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
case 'WebFetch': {
|
|
115
|
+
try {
|
|
116
|
+
return `Fetching ${new URL(String(args.url)).host}`;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return `Fetching ...`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
case 'BrowserBatch':
|
|
123
|
+
return `浏览器 · ${truncate(args.action, 30) || '批量操作'}(${Array.isArray(args.steps) ? args.steps.length : 0} 步)`;
|
|
124
|
+
case 'BrowserCDP':
|
|
125
|
+
return `浏览器调试 · ${truncate(String(args.method ?? args.operation ?? ''), 45)}`;
|
|
126
|
+
case 'WebSearch':
|
|
127
|
+
return `Searching \`${truncate(args.query, 50)}\``;
|
|
128
|
+
case 'Agent': {
|
|
129
|
+
const t = typeof args.agent_type === 'string' ? args.agent_type : 'explore';
|
|
130
|
+
return `子任务[${t}] ${truncate(args.description, 50)}`;
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
return `${toolName} · ${truncate(args.description ?? args.action ?? args.operation, 60) || '执行工具操作'}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/** 给 UI 展示用的参数预览(只取前 60 字符) */
|
|
2
|
+
export function previewArgs(rawJson: string): string {
|
|
3
|
+
const oneLine = rawJson.replace(/\s+/g, ' ').trim();
|
|
4
|
+
if (oneLine.length <= 60) return oneLine;
|
|
5
|
+
return oneLine.slice(0, 60) + '...';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 把工具名 + raw arguments JSON 转换成人话描述。
|
|
10
|
+
* UI 优先用这个显示在 spinner 行(如 "Reading src/foo.ts")。
|
|
11
|
+
* 已知工具走 switch;未知工具或 JSON parse 失败 fallback 到 `${toolName}(...)`。
|
|
12
|
+
*/
|
|
13
|
+
export function friendlyToolDescription(
|
|
14
|
+
toolName: string,
|
|
15
|
+
rawArgsJson: string,
|
|
16
|
+
fileLabel?: (path: string) => string,
|
|
17
|
+
): string {
|
|
18
|
+
let args: Record<string, unknown> = {};
|
|
19
|
+
try {
|
|
20
|
+
args = JSON.parse(rawArgsJson) ?? {};
|
|
21
|
+
} catch {
|
|
22
|
+
return `${toolName}(...)`;
|
|
23
|
+
}
|
|
24
|
+
const truncate = (s: unknown, n: number): string => {
|
|
25
|
+
if (typeof s !== 'string') return '';
|
|
26
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
27
|
+
};
|
|
28
|
+
const file = (value: unknown): string => {
|
|
29
|
+
if (typeof value !== 'string') return '';
|
|
30
|
+
if (fileLabel) return fileLabel(value);
|
|
31
|
+
return value.length > 64 ? `…${value.slice(-63)}` : value;
|
|
32
|
+
};
|
|
33
|
+
switch (toolName) {
|
|
34
|
+
case 'Skill':
|
|
35
|
+
return `技能 · ${truncate(args.name, 60)}`;
|
|
36
|
+
case 'SearchSkills':
|
|
37
|
+
return `查找技能 · ${truncate(args.query, 60)}`;
|
|
38
|
+
case 'Advisor': {
|
|
39
|
+
const reasons: Record<string, string> = {
|
|
40
|
+
plan_review: '执行前评审',
|
|
41
|
+
delivery_review: '交付前核验',
|
|
42
|
+
stalled: '分析重复失败',
|
|
43
|
+
uncertainty: '核查不确定问题',
|
|
44
|
+
user_dissatisfaction: '复核用户反馈',
|
|
45
|
+
evidence_conflict: '核查证据冲突',
|
|
46
|
+
};
|
|
47
|
+
return `咨询顾问 · ${reasons[String(args.reason)] ?? '独立第二意见'}`;
|
|
48
|
+
}
|
|
49
|
+
case 'AdvisorDecision':
|
|
50
|
+
return `顾问建议 · ${truncate(args.decision ?? args.action, 40) || '记录核验与采纳结论'}`;
|
|
51
|
+
case 'AskUserQuestion':
|
|
52
|
+
return '询问关键决策';
|
|
53
|
+
case 'PresentResult':
|
|
54
|
+
return '提交成果与验证结果';
|
|
55
|
+
case 'PowerShell':
|
|
56
|
+
return `运行 PowerShell · ${truncate(args.description ?? args.command, 60)}`;
|
|
57
|
+
case 'OptimizeConversation':
|
|
58
|
+
return '优化对话';
|
|
59
|
+
case 'Read':
|
|
60
|
+
return `Reading ${file(args.file_path)}`;
|
|
61
|
+
case 'Edit':
|
|
62
|
+
return `Editing ${file(args.file_path)}`;
|
|
63
|
+
case 'MultiEdit': {
|
|
64
|
+
const n = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
65
|
+
return `Editing ${file(args.file_path)}${n ? ` (${n} 处)` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
case 'Write':
|
|
68
|
+
return `Writing ${file(args.file_path)}`;
|
|
69
|
+
case 'Trash':
|
|
70
|
+
return `Moving to Trash ${file(args.file_path ?? args.path)}`;
|
|
71
|
+
case 'ApplyPatch': {
|
|
72
|
+
const patch = typeof args.patch === 'string' ? args.patch : typeof args.input === 'string' ? args.input : '';
|
|
73
|
+
const files = [...patch.matchAll(/^\*\*\* (Add|Update|Delete) File: (.+)$/gm)].map(
|
|
74
|
+
(m) => `${file(m[2])}(${({ Add: '新增', Update: '修改', Delete: '删除' } as Record<string, string>)[m[1]]})`,
|
|
75
|
+
);
|
|
76
|
+
if (!files.length) files.push(...[...patch.matchAll(/^\+\+\+ [ab]\/([^\n]+)$/gm)].map((m) => file(m[1])));
|
|
77
|
+
return files.length > 0 ? `Editing ${files.join(', ')}` : 'Editing patch';
|
|
78
|
+
}
|
|
79
|
+
case 'NotebookEdit':
|
|
80
|
+
return `Editing ${file(args.notebook_path ?? args.file_path)}`;
|
|
81
|
+
case 'Bash':
|
|
82
|
+
return typeof args.description === 'string' && args.description.trim()
|
|
83
|
+
? `运行 ${truncate(args.description, 70)}`
|
|
84
|
+
: `Running \`${truncate(args.command, 40)}\``;
|
|
85
|
+
case 'Grep': {
|
|
86
|
+
const p = truncate(args.pattern, 30);
|
|
87
|
+
const g = typeof args.glob === 'string' && args.glob.length > 0 ? ` in ${truncate(args.glob, 25)}` : '';
|
|
88
|
+
return `Searching \`${p}\`${g}`;
|
|
89
|
+
}
|
|
90
|
+
case 'Glob':
|
|
91
|
+
return `Globbing \`${truncate(args.pattern, 50)}\``;
|
|
92
|
+
case 'WebBrowser': {
|
|
93
|
+
const actions: Record<string, string> = {
|
|
94
|
+
start: '启动',
|
|
95
|
+
navigate: '打开页面',
|
|
96
|
+
screenshot: '截图',
|
|
97
|
+
snapshot: '读取页面',
|
|
98
|
+
click: '点击',
|
|
99
|
+
fill: '填写',
|
|
100
|
+
evaluate: '执行页面脚本',
|
|
101
|
+
close: '关闭',
|
|
102
|
+
stop: '停止',
|
|
103
|
+
console: '查看控制台',
|
|
104
|
+
network: '查看网络请求',
|
|
105
|
+
};
|
|
106
|
+
let target = '';
|
|
107
|
+
try {
|
|
108
|
+
target = new URL(String(args.url)).host;
|
|
109
|
+
} catch {
|
|
110
|
+
/* no URL for local actions */
|
|
111
|
+
}
|
|
112
|
+
return `浏览器 · ${actions[String(args.action)] ?? (truncate(args.action, 32) || '操作页面')}${target ? ` · ${target}` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
case 'WebFetch': {
|
|
115
|
+
try {
|
|
116
|
+
return `Fetching ${new URL(String(args.url)).host}`;
|
|
117
|
+
} catch {
|
|
118
|
+
return `Fetching ...`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
case 'BrowserBatch':
|
|
122
|
+
return `浏览器 · ${truncate(args.action, 30) || '批量操作'}(${Array.isArray(args.steps) ? args.steps.length : 0} 步)`;
|
|
123
|
+
case 'BrowserCDP':
|
|
124
|
+
return `浏览器调试 · ${truncate(String(args.method ?? args.operation ?? ''), 45)}`;
|
|
125
|
+
case 'WebSearch':
|
|
126
|
+
return `Searching \`${truncate(args.query, 50)}\``;
|
|
127
|
+
case 'Agent': {
|
|
128
|
+
const t = typeof args.agent_type === 'string' ? args.agent_type : 'explore';
|
|
129
|
+
return `子任务[${t}] ${truncate(args.description, 50)}`;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
return `${toolName} · ${truncate(args.description ?? args.action ?? args.operation, 60) || '执行工具操作'}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isReadOnlyShellCommand, isVerificationShellCommand, isAdvisorInspectionCommand } from '../tools/bash/semantics.js';
|
|
2
|
+
import { toolEffectFor } from '../tools/capabilities.js';
|
|
3
|
+
import { getToolByName, getToolGateDetails } from '../tools/index.js';
|
|
4
|
+
export function advisorSemanticsFor(call, context, tool,
|
|
5
|
+
// 默认 false:只有 advisor 检查点/记账(beforeTool、observeToolResult、
|
|
6
|
+
// 持久化的 advisorMutatesWorkspace 标注)才传 true。工作区租约判定
|
|
7
|
+
// (needsWorkspaceLease)必须保持严格定义不受影响——验证类命令仍可能写
|
|
8
|
+
// 构建产物/缓存,并发场景下让它绕开租约会重新引入竞态。
|
|
9
|
+
opts) {
|
|
10
|
+
const definition = tool ?? getToolByName(call.function.name);
|
|
11
|
+
const gate = context ? getToolGateDetails(call.function.name, call.function.arguments, context) : undefined;
|
|
12
|
+
// Reuse the tool's side-effect contract, including shell/scripts, Trash and
|
|
13
|
+
// generation tools. Shell tools refine their generic writable declaration
|
|
14
|
+
// using the actual command; the same result is also used by the host gate.
|
|
15
|
+
let isReadOnly = gate?.isReadOnly ?? definition?.isReadOnly ?? false;
|
|
16
|
+
if (call.function.name === 'Agent') {
|
|
17
|
+
try {
|
|
18
|
+
const args = JSON.parse(call.function.arguments || '{}');
|
|
19
|
+
isReadOnly = !args.resume_agent_id && (args.agent_type ?? 'explore') === 'explore';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
isReadOnly = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (call.function.name === 'Bash' || call.function.name === 'PowerShell') {
|
|
26
|
+
try {
|
|
27
|
+
const input = JSON.parse(call.function.arguments || '{}');
|
|
28
|
+
if (typeof input.command === 'string') {
|
|
29
|
+
// 跑测试/构建/lint 只是核对现状,不该被当成"开始实施改动"而白白烧掉
|
|
30
|
+
// pre-execution 检查点——即使它们可能写构建产物/缓存,不满足
|
|
31
|
+
// isReadOnlyShellCommand 的并发安全定义(见 opts 注释,那个场景仍要严格)。
|
|
32
|
+
isReadOnly =
|
|
33
|
+
isReadOnlyShellCommand(input.command) ||
|
|
34
|
+
(opts?.treatVerificationAsReadOnly === true &&
|
|
35
|
+
(isAdvisorInspectionCommand(input.command) || isVerificationShellCommand(input.command)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Malformed input is left writable; normal tool validation reports it.
|
|
40
|
+
isReadOnly = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const mutatesWorkspace = toolEffectFor(call.function.name, isReadOnly) === 'execution';
|
|
44
|
+
return { mutatesWorkspace };
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AdvisorToolSemantics } from '../advisor/advisorCoordinator.js';
|
|
2
|
+
import { isReadOnlyShellCommand, isVerificationShellCommand, isAdvisorInspectionCommand } from '../tools/bash/semantics.js';
|
|
3
|
+
import { toolEffectFor } from '../tools/capabilities.js';
|
|
4
|
+
import { getToolByName, getToolGateDetails } from '../tools/index.js';
|
|
5
|
+
import type { Tool, ToolCall, ToolCallContext } from '../types.js';
|
|
6
|
+
export function advisorSemanticsFor(
|
|
7
|
+
call: ToolCall,
|
|
8
|
+
context?: ToolCallContext,
|
|
9
|
+
tool?: Tool,
|
|
10
|
+
// 默认 false:只有 advisor 检查点/记账(beforeTool、observeToolResult、
|
|
11
|
+
// 持久化的 advisorMutatesWorkspace 标注)才传 true。工作区租约判定
|
|
12
|
+
// (needsWorkspaceLease)必须保持严格定义不受影响——验证类命令仍可能写
|
|
13
|
+
// 构建产物/缓存,并发场景下让它绕开租约会重新引入竞态。
|
|
14
|
+
opts?: { treatVerificationAsReadOnly?: boolean },
|
|
15
|
+
): AdvisorToolSemantics {
|
|
16
|
+
const definition = tool ?? getToolByName(call.function.name);
|
|
17
|
+
const gate = context ? getToolGateDetails(call.function.name, call.function.arguments, context) : undefined;
|
|
18
|
+
// Reuse the tool's side-effect contract, including shell/scripts, Trash and
|
|
19
|
+
// generation tools. Shell tools refine their generic writable declaration
|
|
20
|
+
// using the actual command; the same result is also used by the host gate.
|
|
21
|
+
let isReadOnly = gate?.isReadOnly ?? definition?.isReadOnly ?? false;
|
|
22
|
+
if (call.function.name === 'Agent') {
|
|
23
|
+
try {
|
|
24
|
+
const args = JSON.parse(call.function.arguments || '{}');
|
|
25
|
+
isReadOnly = !args.resume_agent_id && (args.agent_type ?? 'explore') === 'explore';
|
|
26
|
+
} catch {
|
|
27
|
+
isReadOnly = false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (call.function.name === 'Bash' || call.function.name === 'PowerShell') {
|
|
31
|
+
try {
|
|
32
|
+
const input = JSON.parse(call.function.arguments || '{}') as {
|
|
33
|
+
command?: unknown;
|
|
34
|
+
};
|
|
35
|
+
if (typeof input.command === 'string') {
|
|
36
|
+
// 跑测试/构建/lint 只是核对现状,不该被当成"开始实施改动"而白白烧掉
|
|
37
|
+
// pre-execution 检查点——即使它们可能写构建产物/缓存,不满足
|
|
38
|
+
// isReadOnlyShellCommand 的并发安全定义(见 opts 注释,那个场景仍要严格)。
|
|
39
|
+
isReadOnly =
|
|
40
|
+
isReadOnlyShellCommand(input.command) ||
|
|
41
|
+
(opts?.treatVerificationAsReadOnly === true &&
|
|
42
|
+
(isAdvisorInspectionCommand(input.command) || isVerificationShellCommand(input.command)));
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Malformed input is left writable; normal tool validation reports it.
|
|
46
|
+
isReadOnly = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const mutatesWorkspace = toolEffectFor(call.function.name, isReadOnly) === 'execution';
|
|
50
|
+
return { mutatesWorkspace };
|
|
51
|
+
}
|