@lobehub/chat 1.141.7 → 1.141.9
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 +50 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +279 -52
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +392 -0
- package/changelog/v1.json +18 -0
- package/docs/usage/features/{group-chat.mdx → agent-team.mdx} +14 -14
- package/docs/usage/features/agent-team.zh-CN.mdx +52 -0
- package/locales/ar/chat.json +17 -17
- package/locales/ar/setting.json +15 -19
- package/locales/ar/welcome.json +1 -1
- package/locales/bg-BG/chat.json +17 -17
- package/locales/bg-BG/setting.json +15 -19
- package/locales/de-DE/chat.json +17 -17
- package/locales/de-DE/setting.json +15 -19
- package/locales/de-DE/welcome.json +1 -1
- package/locales/en-US/chat.json +17 -17
- package/locales/en-US/setting.json +15 -19
- package/locales/en-US/welcome.json +1 -1
- package/locales/es-ES/chat.json +17 -17
- package/locales/es-ES/setting.json +15 -19
- package/locales/es-ES/welcome.json +1 -1
- package/locales/fa-IR/chat.json +17 -17
- package/locales/fa-IR/setting.json +15 -19
- package/locales/fa-IR/welcome.json +1 -1
- package/locales/fr-FR/chat.json +16 -16
- package/locales/fr-FR/setting.json +15 -19
- package/locales/fr-FR/welcome.json +1 -1
- package/locales/it-IT/chat.json +17 -17
- package/locales/it-IT/setting.json +15 -19
- package/locales/it-IT/welcome.json +1 -1
- package/locales/ja-JP/chat.json +17 -17
- package/locales/ja-JP/setting.json +15 -19
- package/locales/ja-JP/welcome.json +1 -1
- package/locales/ko-KR/chat.json +17 -17
- package/locales/ko-KR/setting.json +15 -19
- package/locales/ko-KR/welcome.json +1 -1
- package/locales/nl-NL/chat.json +17 -17
- package/locales/nl-NL/setting.json +15 -19
- package/locales/nl-NL/welcome.json +1 -1
- package/locales/pl-PL/chat.json +17 -17
- package/locales/pl-PL/setting.json +15 -19
- package/locales/pt-BR/chat.json +17 -17
- package/locales/pt-BR/setting.json +15 -19
- package/locales/pt-BR/welcome.json +1 -1
- package/locales/ru-RU/chat.json +17 -17
- package/locales/ru-RU/setting.json +15 -19
- package/locales/ru-RU/welcome.json +1 -1
- package/locales/tr-TR/chat.json +17 -17
- package/locales/tr-TR/setting.json +15 -19
- package/locales/vi-VN/chat.json +15 -15
- package/locales/vi-VN/setting.json +15 -19
- package/locales/zh-CN/chat.json +17 -17
- package/locales/zh-CN/setting.json +15 -19
- package/locales/zh-CN/welcome.json +1 -1
- package/locales/zh-TW/chat.json +17 -17
- package/locales/zh-TW/setting.json +15 -19
- package/locales/zh-TW/welcome.json +1 -1
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/InterventionChecker.ts +173 -0
- package/packages/agent-runtime/src/core/UsageCounter.ts +248 -0
- package/packages/agent-runtime/src/core/__tests__/InterventionChecker.test.ts +334 -0
- package/packages/agent-runtime/src/core/__tests__/UsageCounter.test.ts +873 -0
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +32 -26
- package/packages/agent-runtime/src/core/index.ts +2 -0
- package/packages/agent-runtime/src/core/runtime.ts +31 -18
- package/packages/agent-runtime/src/types/instruction.ts +1 -1
- package/packages/agent-runtime/src/types/state.ts +3 -3
- package/packages/agent-runtime/src/types/usage.ts +34 -25
- package/packages/const/src/settings/systemAgent.ts +0 -1
- package/packages/context-engine/src/index.ts +1 -0
- package/packages/context-engine/src/tools/ToolNameResolver.ts +2 -2
- package/packages/context-engine/src/tools/ToolsEngine.ts +37 -8
- package/packages/context-engine/src/tools/__tests__/ToolsEngine.test.ts +149 -5
- package/packages/context-engine/src/tools/__tests__/utils.test.ts +2 -2
- package/packages/context-engine/src/tools/index.ts +1 -0
- package/packages/context-engine/src/tools/types.ts +18 -3
- package/packages/context-engine/src/tools/utils.ts +4 -4
- package/packages/types/src/tool/builtin.ts +54 -1
- package/packages/types/src/tool/index.ts +1 -0
- package/packages/types/src/tool/intervention.ts +114 -0
- package/packages/types/src/user/settings/systemAgent.ts +0 -1
- package/packages/types/src/user/settings/tool.ts +37 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/OrchestratorThinking.tsx +2 -3
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/GroupConfig/GroupMember.tsx +34 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +1 -1
- package/src/app/[variants]/(main)/chat/(workspace)/features/{GroupChatSettings → AgentTeamSettings}/index.tsx +4 -5
- package/src/app/[variants]/(main)/chat/(workspace)/features/SettingButton.tsx +2 -2
- package/src/app/[variants]/(main)/chat/@session/_layout/Desktop/SessionHeader.tsx +2 -0
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/CollapseGroup/Actions.tsx +18 -1
- package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +33 -5
- package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +170 -26
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +7 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +4 -2
- package/src/features/Conversation/Messages/User/Actions.tsx +8 -2
- package/src/features/GroupChatSettings/{ChatGroupSettings.tsx → AgentTeamChatSettings.tsx} +6 -5
- package/src/features/GroupChatSettings/{GroupMembers.tsx → AgentTeamMembersSettings.tsx} +64 -19
- package/src/features/GroupChatSettings/{ChatGroupMeta.tsx → AgentTeamMetaSettings.tsx} +2 -2
- package/src/features/GroupChatSettings/AgentTeamSettings.tsx +54 -0
- package/src/features/GroupChatSettings/index.ts +4 -5
- package/src/locales/default/chat.ts +17 -17
- package/src/locales/default/setting.ts +15 -19
- package/src/locales/default/welcome.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +2 -1
- package/src/store/chat/slices/builtinTool/actions/{dalle.test.ts → __tests__/dalle.test.ts} +2 -5
- package/src/store/chat/slices/builtinTool/actions/__tests__/{localFile.test.ts → localSystem.test.ts} +4 -4
- package/src/store/chat/slices/builtinTool/actions/index.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/{localFile.ts → localSystem.ts} +183 -69
- package/src/store/chatGroup/action.ts +36 -1
- package/src/store/electron/selectors/__tests__/desktopState.test.ts +3 -3
- package/src/store/electron/selectors/desktopState.ts +11 -2
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +0 -4
- package/src/store/user/slices/settings/selectors/systemAgent.ts +0 -2
- package/src/tools/local-system/Placeholder/ListFiles.tsx +10 -8
- package/src/tools/local-system/Placeholder/SearchFiles.tsx +12 -10
- package/src/tools/local-system/Placeholder/index.tsx +1 -1
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileSkeleton.tsx +8 -18
- package/src/tools/local-system/Render/ReadLocalFile/ReadFileView.tsx +21 -6
- package/src/tools/local-system/Render/SearchFiles/Result.tsx +5 -4
- package/src/tools/local-system/Render/SearchFiles/SearchQuery/SearchView.tsx +4 -15
- package/src/tools/local-system/Render/SearchFiles/index.tsx +3 -2
- package/src/tools/local-system/type.ts +39 -0
- package/docs/usage/features/group-chat.zh-CN.mdx +0 -52
- package/src/features/GroupChatSettings/GroupSettings.tsx +0 -30
- package/src/features/GroupChatSettings/GroupSettingsContent.tsx +0 -24
- package/src/tools/local-system/Placeholder/ReadLocalFile.tsx +0 -9
- package/src/tools/local-system/Render/ReadLocalFile/style.ts +0 -37
- /package/src/store/chat/slices/builtinTool/actions/{search.test.ts → __tests__/search.test.ts} +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { ModelUsage } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
import { Cost, Usage } from '../types/usage';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UsageCounter - Pure accumulator for usage and cost tracking
|
|
7
|
+
* Focuses only on usage/cost calculations without managing state
|
|
8
|
+
*/
|
|
9
|
+
/* eslint-disable unicorn/no-static-only-class */
|
|
10
|
+
export class UsageCounter {
|
|
11
|
+
/**
|
|
12
|
+
* Create default usage statistics
|
|
13
|
+
*/
|
|
14
|
+
private static createDefaultUsage(): Usage {
|
|
15
|
+
return {
|
|
16
|
+
humanInteraction: {
|
|
17
|
+
approvalRequests: 0,
|
|
18
|
+
promptRequests: 0,
|
|
19
|
+
selectRequests: 0,
|
|
20
|
+
totalWaitingTimeMs: 0,
|
|
21
|
+
},
|
|
22
|
+
llm: {
|
|
23
|
+
apiCalls: 0,
|
|
24
|
+
processingTimeMs: 0,
|
|
25
|
+
tokens: {
|
|
26
|
+
input: 0,
|
|
27
|
+
output: 0,
|
|
28
|
+
total: 0,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
tools: {
|
|
32
|
+
byTool: [],
|
|
33
|
+
totalCalls: 0,
|
|
34
|
+
totalTimeMs: 0,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create default cost statistics
|
|
41
|
+
*/
|
|
42
|
+
private static createDefaultCost(): Cost {
|
|
43
|
+
return {
|
|
44
|
+
calculatedAt: new Date().toISOString(),
|
|
45
|
+
currency: 'USD',
|
|
46
|
+
llm: {
|
|
47
|
+
byModel: [],
|
|
48
|
+
currency: 'USD',
|
|
49
|
+
total: 0,
|
|
50
|
+
},
|
|
51
|
+
tools: {
|
|
52
|
+
byTool: [],
|
|
53
|
+
currency: 'USD',
|
|
54
|
+
total: 0,
|
|
55
|
+
},
|
|
56
|
+
total: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Merge two ModelUsage objects by accumulating token counts
|
|
62
|
+
* @param previous - Previous usage statistics
|
|
63
|
+
* @param current - Current usage statistics to add
|
|
64
|
+
* @returns Merged usage statistics
|
|
65
|
+
*/
|
|
66
|
+
private static mergeModelUsage(
|
|
67
|
+
previous: ModelUsage | undefined,
|
|
68
|
+
current: ModelUsage,
|
|
69
|
+
): ModelUsage {
|
|
70
|
+
if (!previous) return current;
|
|
71
|
+
|
|
72
|
+
const merged: ModelUsage = { ...current };
|
|
73
|
+
|
|
74
|
+
// Accumulate all numeric token fields
|
|
75
|
+
const numericFields: (keyof ModelUsage)[] = [
|
|
76
|
+
'inputCachedTokens',
|
|
77
|
+
'inputCacheMissTokens',
|
|
78
|
+
'inputWriteCacheTokens',
|
|
79
|
+
'inputTextTokens',
|
|
80
|
+
'inputImageTokens',
|
|
81
|
+
'inputAudioTokens',
|
|
82
|
+
'inputCitationTokens',
|
|
83
|
+
'outputTextTokens',
|
|
84
|
+
'outputImageTokens',
|
|
85
|
+
'outputAudioTokens',
|
|
86
|
+
'outputReasoningTokens',
|
|
87
|
+
'acceptedPredictionTokens',
|
|
88
|
+
'rejectedPredictionTokens',
|
|
89
|
+
'totalInputTokens',
|
|
90
|
+
'totalOutputTokens',
|
|
91
|
+
'totalTokens',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const field of numericFields) {
|
|
95
|
+
const prevValue = previous[field] as number | undefined;
|
|
96
|
+
const currValue = current[field] as number | undefined;
|
|
97
|
+
|
|
98
|
+
if (prevValue !== undefined || currValue !== undefined) {
|
|
99
|
+
merged[field] = (prevValue || 0) + (currValue || 0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Accumulate cost
|
|
104
|
+
if (previous.cost !== undefined || current.cost !== undefined) {
|
|
105
|
+
merged.cost = (previous.cost || 0) + (current.cost || 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return merged;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Accumulate LLM usage and cost for a specific model
|
|
113
|
+
* @param params - Accumulation parameters
|
|
114
|
+
* @param params.usage - Current usage statistics (optional, will be created if not provided)
|
|
115
|
+
* @param params.cost - Current cost statistics (optional, will be created if not provided)
|
|
116
|
+
* @param params.provider - Provider name (e.g., "openai")
|
|
117
|
+
* @param params.model - Model name (e.g., "gpt-4")
|
|
118
|
+
* @param params.modelUsage - ModelUsage from model-runtime
|
|
119
|
+
* @returns Updated usage and cost
|
|
120
|
+
*/
|
|
121
|
+
static accumulateLLM(params: {
|
|
122
|
+
cost?: Cost;
|
|
123
|
+
model: string;
|
|
124
|
+
modelUsage: ModelUsage;
|
|
125
|
+
provider: string;
|
|
126
|
+
usage?: Usage;
|
|
127
|
+
}): { cost?: Cost; usage: Usage } {
|
|
128
|
+
const { usage, cost, provider, model, modelUsage } = params;
|
|
129
|
+
|
|
130
|
+
// Ensure usage exists
|
|
131
|
+
const newUsage = usage ? structuredClone(usage) : this.createDefaultUsage();
|
|
132
|
+
|
|
133
|
+
// Accumulate token counts to usage.llm
|
|
134
|
+
newUsage.llm.tokens.input += modelUsage.totalInputTokens ?? 0;
|
|
135
|
+
newUsage.llm.tokens.output += modelUsage.totalOutputTokens ?? 0;
|
|
136
|
+
newUsage.llm.tokens.total += modelUsage.totalTokens ?? 0;
|
|
137
|
+
newUsage.llm.apiCalls += 1;
|
|
138
|
+
|
|
139
|
+
// Ensure cost exists if modelUsage has cost
|
|
140
|
+
let newCost = cost
|
|
141
|
+
? structuredClone(cost)
|
|
142
|
+
: modelUsage.cost
|
|
143
|
+
? this.createDefaultCost()
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
if (modelUsage.cost && newCost) {
|
|
147
|
+
const modelId = `${provider}/${model}`;
|
|
148
|
+
|
|
149
|
+
// Find or create byModel entry
|
|
150
|
+
let modelEntry = newCost.llm.byModel.find((entry) => entry.id === modelId);
|
|
151
|
+
|
|
152
|
+
if (!modelEntry) {
|
|
153
|
+
modelEntry = {
|
|
154
|
+
id: modelId,
|
|
155
|
+
model,
|
|
156
|
+
provider,
|
|
157
|
+
totalCost: 0,
|
|
158
|
+
usage: {},
|
|
159
|
+
};
|
|
160
|
+
newCost.llm.byModel.push(modelEntry);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Merge usage breakdown
|
|
164
|
+
modelEntry.usage = UsageCounter.mergeModelUsage(modelEntry.usage, modelUsage);
|
|
165
|
+
|
|
166
|
+
// Accumulate costs
|
|
167
|
+
modelEntry.totalCost += modelUsage.cost;
|
|
168
|
+
newCost.llm.total += modelUsage.cost;
|
|
169
|
+
newCost.total += modelUsage.cost;
|
|
170
|
+
newCost.calculatedAt = new Date().toISOString();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { cost: newCost, usage: newUsage };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Accumulate tool usage and cost
|
|
178
|
+
* @param params - Accumulation parameters
|
|
179
|
+
* @param params.usage - Current usage statistics (optional, will be created if not provided)
|
|
180
|
+
* @param params.cost - Current cost statistics (optional, will be created if not provided)
|
|
181
|
+
* @param params.toolName - Tool identifier
|
|
182
|
+
* @param params.executionTime - Execution time in milliseconds
|
|
183
|
+
* @param params.success - Whether the execution was successful
|
|
184
|
+
* @param params.toolCost - Optional cost for this tool call
|
|
185
|
+
* @returns Updated usage and cost
|
|
186
|
+
*/
|
|
187
|
+
static accumulateTool(params: {
|
|
188
|
+
cost?: Cost;
|
|
189
|
+
executionTime: number;
|
|
190
|
+
success: boolean;
|
|
191
|
+
toolCost?: number;
|
|
192
|
+
toolName: string;
|
|
193
|
+
usage?: Usage;
|
|
194
|
+
}): { cost?: Cost; usage: Usage } {
|
|
195
|
+
const { usage, cost, toolName, executionTime, success, toolCost } = params;
|
|
196
|
+
|
|
197
|
+
// Ensure usage exists
|
|
198
|
+
const newUsage = usage ? structuredClone(usage) : this.createDefaultUsage();
|
|
199
|
+
|
|
200
|
+
// Find or create byTool entry
|
|
201
|
+
let toolEntry = newUsage.tools.byTool.find((entry) => entry.name === toolName);
|
|
202
|
+
|
|
203
|
+
if (!toolEntry) {
|
|
204
|
+
toolEntry = {
|
|
205
|
+
calls: 0,
|
|
206
|
+
errors: 0,
|
|
207
|
+
name: toolName,
|
|
208
|
+
totalTimeMs: 0,
|
|
209
|
+
};
|
|
210
|
+
newUsage.tools.byTool.push(toolEntry);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Accumulate tool usage
|
|
214
|
+
toolEntry.calls += 1;
|
|
215
|
+
toolEntry.totalTimeMs += executionTime;
|
|
216
|
+
if (!success) {
|
|
217
|
+
toolEntry.errors += 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
newUsage.tools.totalCalls += 1;
|
|
221
|
+
newUsage.tools.totalTimeMs += executionTime;
|
|
222
|
+
|
|
223
|
+
// Ensure cost exists if toolCost is provided
|
|
224
|
+
let newCost = cost ? structuredClone(cost) : toolCost ? this.createDefaultCost() : undefined;
|
|
225
|
+
|
|
226
|
+
if (toolCost && newCost) {
|
|
227
|
+
let toolCostEntry = newCost.tools.byTool.find((entry) => entry.name === toolName);
|
|
228
|
+
|
|
229
|
+
if (!toolCostEntry) {
|
|
230
|
+
toolCostEntry = {
|
|
231
|
+
calls: 0,
|
|
232
|
+
currency: 'USD',
|
|
233
|
+
name: toolName,
|
|
234
|
+
totalCost: 0,
|
|
235
|
+
};
|
|
236
|
+
newCost.tools.byTool.push(toolCostEntry);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
toolCostEntry.calls += 1;
|
|
240
|
+
toolCostEntry.totalCost += toolCost;
|
|
241
|
+
newCost.tools.total += toolCost;
|
|
242
|
+
newCost.total += toolCost;
|
|
243
|
+
newCost.calculatedAt = new Date().toISOString();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { cost: newCost, usage: newUsage };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { HumanInterventionConfig } from '@lobechat/types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { InterventionChecker } from '../InterventionChecker';
|
|
5
|
+
|
|
6
|
+
describe('InterventionChecker', () => {
|
|
7
|
+
describe('shouldIntervene', () => {
|
|
8
|
+
it('should return never when config is undefined', () => {
|
|
9
|
+
const result = InterventionChecker.shouldIntervene({ config: undefined, toolArgs: {} });
|
|
10
|
+
expect(result).toBe('never');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return the policy when config is a simple string', () => {
|
|
14
|
+
expect(InterventionChecker.shouldIntervene({ config: 'never', toolArgs: {} })).toBe('never');
|
|
15
|
+
expect(InterventionChecker.shouldIntervene({ config: 'always', toolArgs: {} })).toBe(
|
|
16
|
+
'always',
|
|
17
|
+
);
|
|
18
|
+
expect(InterventionChecker.shouldIntervene({ config: 'first', toolArgs: {} })).toBe('first');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should handle "first" policy with confirmed history', () => {
|
|
22
|
+
const toolKey = 'web-browsing/crawlSinglePage';
|
|
23
|
+
const confirmedHistory = [toolKey];
|
|
24
|
+
|
|
25
|
+
const result = InterventionChecker.shouldIntervene({
|
|
26
|
+
config: 'first',
|
|
27
|
+
toolArgs: {},
|
|
28
|
+
confirmedHistory,
|
|
29
|
+
toolKey,
|
|
30
|
+
});
|
|
31
|
+
expect(result).toBe('never');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should require intervention for "first" policy without confirmation', () => {
|
|
35
|
+
const toolKey = 'web-browsing/crawlSinglePage';
|
|
36
|
+
const confirmedHistory: string[] = [];
|
|
37
|
+
|
|
38
|
+
const result = InterventionChecker.shouldIntervene({
|
|
39
|
+
config: 'first',
|
|
40
|
+
toolArgs: {},
|
|
41
|
+
confirmedHistory,
|
|
42
|
+
toolKey,
|
|
43
|
+
});
|
|
44
|
+
expect(result).toBe('first');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should match rules in order and return first match', () => {
|
|
48
|
+
const config: HumanInterventionConfig = [
|
|
49
|
+
{ match: { command: 'ls:*' }, policy: 'never' },
|
|
50
|
+
{ match: { command: 'git commit:*' }, policy: 'first' },
|
|
51
|
+
{ policy: 'always' }, // Default rule
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
|
|
55
|
+
'never',
|
|
56
|
+
);
|
|
57
|
+
expect(
|
|
58
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:' } }),
|
|
59
|
+
).toBe('first');
|
|
60
|
+
expect(
|
|
61
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm -rf /' } }),
|
|
62
|
+
).toBe('always');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return always as default when no rule matches', () => {
|
|
66
|
+
const config: HumanInterventionConfig = [{ match: { command: 'ls:*' }, policy: 'never' }];
|
|
67
|
+
|
|
68
|
+
const result = InterventionChecker.shouldIntervene({
|
|
69
|
+
config,
|
|
70
|
+
toolArgs: { command: 'rm -rf /' },
|
|
71
|
+
});
|
|
72
|
+
expect(result).toBe('always');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle multiple parameter matching', () => {
|
|
76
|
+
const config: HumanInterventionConfig = [
|
|
77
|
+
{
|
|
78
|
+
match: {
|
|
79
|
+
command: 'git add:*',
|
|
80
|
+
path: '/Users/project/*',
|
|
81
|
+
},
|
|
82
|
+
policy: 'never',
|
|
83
|
+
},
|
|
84
|
+
{ policy: 'always' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Both match
|
|
88
|
+
expect(
|
|
89
|
+
InterventionChecker.shouldIntervene({
|
|
90
|
+
config,
|
|
91
|
+
toolArgs: {
|
|
92
|
+
command: 'git add:.',
|
|
93
|
+
path: '/Users/project/file.ts',
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
).toBe('never');
|
|
97
|
+
|
|
98
|
+
// Only one matches
|
|
99
|
+
expect(
|
|
100
|
+
InterventionChecker.shouldIntervene({
|
|
101
|
+
config,
|
|
102
|
+
toolArgs: {
|
|
103
|
+
command: 'git add:.',
|
|
104
|
+
path: '/tmp/file.ts',
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
).toBe('always');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle default rule without match', () => {
|
|
111
|
+
const config: HumanInterventionConfig = [
|
|
112
|
+
{ match: { command: 'ls:*' }, policy: 'never' },
|
|
113
|
+
{ policy: 'first' }, // Default rule
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const result = InterventionChecker.shouldIntervene({
|
|
117
|
+
config,
|
|
118
|
+
toolArgs: { command: 'anything' },
|
|
119
|
+
});
|
|
120
|
+
expect(result).toBe('first');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('matchPattern', () => {
|
|
125
|
+
it('should match exact strings', () => {
|
|
126
|
+
expect(InterventionChecker['matchPattern']('hello', 'hello')).toBe(true);
|
|
127
|
+
expect(InterventionChecker['matchPattern']('hello', 'world')).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should match wildcard patterns', () => {
|
|
131
|
+
expect(InterventionChecker['matchPattern']('*.ts', 'file.ts')).toBe(true);
|
|
132
|
+
expect(InterventionChecker['matchPattern']('*.ts', 'file.js')).toBe(false);
|
|
133
|
+
expect(InterventionChecker['matchPattern']('test*', 'test123')).toBe(true);
|
|
134
|
+
expect(InterventionChecker['matchPattern']('test*', 'abc123')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should match colon-based prefix patterns', () => {
|
|
138
|
+
expect(InterventionChecker['matchPattern']('git add:*', 'git add:')).toBe(true);
|
|
139
|
+
expect(InterventionChecker['matchPattern']('git add:*', 'git add:.')).toBe(true);
|
|
140
|
+
expect(InterventionChecker['matchPattern']('git add:*', 'git add:--all')).toBe(true);
|
|
141
|
+
expect(InterventionChecker['matchPattern']('git add:*', 'git commit')).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should match path patterns', () => {
|
|
145
|
+
expect(
|
|
146
|
+
InterventionChecker['matchPattern']('/Users/project/*', '/Users/project/file.ts'),
|
|
147
|
+
).toBe(true);
|
|
148
|
+
expect(InterventionChecker['matchPattern']('/Users/project/*', '/tmp/file.ts')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('matchesArgument', () => {
|
|
153
|
+
it('should match exact type', () => {
|
|
154
|
+
const matcher = { pattern: 'git add', type: 'exact' as const };
|
|
155
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
|
|
156
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add:.')).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should match prefix type', () => {
|
|
160
|
+
const matcher = { pattern: 'git add', type: 'prefix' as const };
|
|
161
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
|
|
162
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add:.')).toBe(true);
|
|
163
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should match wildcard type', () => {
|
|
167
|
+
const matcher = { pattern: 'git *', type: 'wildcard' as const };
|
|
168
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
|
|
169
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(true);
|
|
170
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'npm install')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should match regex type', () => {
|
|
174
|
+
const matcher = { pattern: '^git (add|commit)', type: 'regex' as const };
|
|
175
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git add')).toBe(true);
|
|
176
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git commit')).toBe(true);
|
|
177
|
+
expect(InterventionChecker['matchesArgument'](matcher, 'git push')).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle simple string matcher', () => {
|
|
181
|
+
expect(InterventionChecker['matchesArgument']('git add:*', 'git add:.')).toBe(true);
|
|
182
|
+
expect(InterventionChecker['matchesArgument']('*.ts', 'file.ts')).toBe(true);
|
|
183
|
+
expect(InterventionChecker['matchesArgument']('exact', 'exact')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('generateToolKey', () => {
|
|
188
|
+
it('should generate key without args hash', () => {
|
|
189
|
+
const key = InterventionChecker.generateToolKey('web-browsing', 'crawlSinglePage');
|
|
190
|
+
expect(key).toBe('web-browsing/crawlSinglePage');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should generate key with args hash', () => {
|
|
194
|
+
const key = InterventionChecker.generateToolKey('bash', 'bash', 'a1b2c3');
|
|
195
|
+
expect(key).toBe('bash/bash#a1b2c3');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('hashArguments', () => {
|
|
200
|
+
it('should generate consistent hash for same arguments', () => {
|
|
201
|
+
const args1 = { command: 'ls -la', path: '/tmp' };
|
|
202
|
+
const args2 = { command: 'ls -la', path: '/tmp' };
|
|
203
|
+
|
|
204
|
+
const hash1 = InterventionChecker.hashArguments(args1);
|
|
205
|
+
const hash2 = InterventionChecker.hashArguments(args2);
|
|
206
|
+
|
|
207
|
+
expect(hash1).toBe(hash2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should generate different hash for different arguments', () => {
|
|
211
|
+
const args1 = { command: 'ls -la' };
|
|
212
|
+
const args2 = { command: 'ls -l' };
|
|
213
|
+
|
|
214
|
+
const hash1 = InterventionChecker.hashArguments(args1);
|
|
215
|
+
const hash2 = InterventionChecker.hashArguments(args2);
|
|
216
|
+
|
|
217
|
+
expect(hash1).not.toBe(hash2);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle key order independence', () => {
|
|
221
|
+
const args1 = { a: 1, b: 2 };
|
|
222
|
+
const args2 = { b: 2, a: 1 };
|
|
223
|
+
|
|
224
|
+
const hash1 = InterventionChecker.hashArguments(args1);
|
|
225
|
+
const hash2 = InterventionChecker.hashArguments(args2);
|
|
226
|
+
|
|
227
|
+
expect(hash1).toBe(hash2);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle empty arguments', () => {
|
|
231
|
+
const hash = InterventionChecker.hashArguments({});
|
|
232
|
+
expect(hash).toBeDefined();
|
|
233
|
+
expect(typeof hash).toBe('string');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle complex nested objects', () => {
|
|
237
|
+
const args = {
|
|
238
|
+
config: { nested: { value: 'test' } },
|
|
239
|
+
array: [1, 2, 3],
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const hash = InterventionChecker.hashArguments(args);
|
|
243
|
+
expect(hash).toBeDefined();
|
|
244
|
+
expect(typeof hash).toBe('string');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('Integration scenarios', () => {
|
|
249
|
+
it('should handle Bash tool scenario', () => {
|
|
250
|
+
const config: HumanInterventionConfig = [
|
|
251
|
+
{ match: { command: 'ls:*' }, policy: 'never' },
|
|
252
|
+
{ match: { command: 'git add:*' }, policy: 'first' },
|
|
253
|
+
{ match: { command: 'git commit:*' }, policy: 'first' },
|
|
254
|
+
{ match: { command: 'rm:*' }, policy: 'always' },
|
|
255
|
+
{ policy: 'always' },
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// Safe commands - never
|
|
259
|
+
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'ls:' } })).toBe(
|
|
260
|
+
'never',
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Git commands - first
|
|
264
|
+
expect(
|
|
265
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git add:.' } }),
|
|
266
|
+
).toBe('first');
|
|
267
|
+
expect(
|
|
268
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'git commit:-m' } }),
|
|
269
|
+
).toBe('first');
|
|
270
|
+
|
|
271
|
+
// Dangerous commands - always
|
|
272
|
+
expect(InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'rm:-rf' } })).toBe(
|
|
273
|
+
'always',
|
|
274
|
+
);
|
|
275
|
+
expect(
|
|
276
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { command: 'npm install' } }),
|
|
277
|
+
).toBe('always');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle LocalSystem tool scenario', () => {
|
|
281
|
+
const config: HumanInterventionConfig = [
|
|
282
|
+
{ match: { path: '/Users/project/*' }, policy: 'never' },
|
|
283
|
+
{ policy: 'first' },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
// Project directory - never
|
|
287
|
+
expect(
|
|
288
|
+
InterventionChecker.shouldIntervene({
|
|
289
|
+
config,
|
|
290
|
+
toolArgs: { path: '/Users/project/file.ts' },
|
|
291
|
+
}),
|
|
292
|
+
).toBe('never');
|
|
293
|
+
|
|
294
|
+
// Outside project - first
|
|
295
|
+
expect(
|
|
296
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { path: '/tmp/file.ts' } }),
|
|
297
|
+
).toBe('first');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should handle Web Browsing tool with simple policy', () => {
|
|
301
|
+
const config: HumanInterventionConfig = 'always';
|
|
302
|
+
|
|
303
|
+
expect(
|
|
304
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: { url: 'https://example.com' } }),
|
|
305
|
+
).toBe('always');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should handle first policy with confirmation history', () => {
|
|
309
|
+
const config: HumanInterventionConfig = [
|
|
310
|
+
{ match: { command: 'git add:*' }, policy: 'first' },
|
|
311
|
+
{ policy: 'always' },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const toolKey = 'bash/bash#abc123';
|
|
315
|
+
const args = { command: 'git add:.' };
|
|
316
|
+
|
|
317
|
+
// First time - requires intervention
|
|
318
|
+
expect(
|
|
319
|
+
InterventionChecker.shouldIntervene({
|
|
320
|
+
config,
|
|
321
|
+
toolArgs: args,
|
|
322
|
+
confirmedHistory: [],
|
|
323
|
+
toolKey,
|
|
324
|
+
}),
|
|
325
|
+
).toBe('first');
|
|
326
|
+
|
|
327
|
+
// After confirmation - never
|
|
328
|
+
const confirmedHistory = [toolKey];
|
|
329
|
+
expect(
|
|
330
|
+
InterventionChecker.shouldIntervene({ config, toolArgs: args, confirmedHistory, toolKey }),
|
|
331
|
+
).toBe('never');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|