Skip to content

Commit b457062

Browse files
committed
feat(agent): add tool parameter throttling and improve approval logic
- Add throttling mechanism (50ms) for streaming tool parameter updates to reduce excessive state updates - Track lastUpdateTime for each streaming tool call to implement throttle logic - Improve needsApproval() function to check granular autoApprove settings (terminal, dangerous) instead of global flag - Add tool calls to toolCalls array during streaming completion and tool_calls_done events for proper loop.ts integration - Remove verbose debug logging from tool execution flow for cleaner logs - Refactor tool execution status updates with clearer variable naming - Ensure tool call deduplication when adding to toolCalls array to prevent duplicates
1 parent b29ee05 commit b457062

File tree

4 files changed

+127
-51
lines changed

4 files changed

+127
-51
lines changed

src/renderer/agent/core/stream.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
7878
let isCleanedUp = false
7979

8080
// 工具调用流式状态
81-
const streamingToolCalls = new Map<string, { id: string; name: string; argsString: string }>()
81+
const streamingToolCalls = new Map<string, { id: string; name: string; argsString: string; lastUpdateTime: number }>()
82+
83+
// 节流:工具参数更新(避免过于频繁的状态更新)
84+
const TOOL_UPDATE_THROTTLE_MS = 50 // 每 50ms 最多更新一次
8285

8386
// 清理函数列表
8487
const cleanups: (() => void)[] = []
@@ -176,7 +179,7 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
176179
isInReasoning = false
177180
}
178181

179-
streamingToolCalls.set(toolId, { id: toolId, name: toolName, argsString: '' })
182+
streamingToolCalls.set(toolId, { id: toolId, name: toolName, argsString: '', lastUpdateTime: 0 })
180183

181184
// 立即添加到 UI
182185
if (assistantId) {
@@ -199,13 +202,23 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
199202
if (tc) {
200203
if (argsDelta) {
201204
tc.argsString += argsDelta
205+
206+
// 节流更新:第一次立即更新,后续根据时间间隔节流
202207
if (assistantId) {
203-
const partialArgs = parsePartialJsonArgs(tc.argsString)
204-
if (partialArgs && Object.keys(partialArgs).length > 0) {
205-
store.updateToolCall(assistantId, tc.id, {
206-
arguments: { ...partialArgs, _streaming: true },
207-
})
208+
const now = Date.now()
209+
const timeSinceLastUpdate = now - tc.lastUpdateTime
210+
211+
// 第一次更新(lastUpdateTime === 0)或距离上次更新超过阈值时,立即更新
212+
if (tc.lastUpdateTime === 0 || timeSinceLastUpdate >= TOOL_UPDATE_THROTTLE_MS) {
213+
tc.lastUpdateTime = now
214+
const partialArgs = parsePartialJsonArgs(tc.argsString)
215+
if (partialArgs && Object.keys(partialArgs).length > 0) {
216+
store.updateToolCall(assistantId, tc.id, {
217+
arguments: { ...partialArgs, _streaming: true },
218+
})
219+
}
208220
}
221+
// 否则跳过此次更新(节流)
209222
}
210223
}
211224
if (data.name && data.name !== tc.name) {
@@ -225,13 +238,26 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
225238
if (tcId && assistantId) {
226239
const tc = streamingToolCalls.get(tcId)
227240
if (tc) {
228-
// 参数传输完成,解析最终参数(移除 _streaming 标记)
241+
// 参数传输完成,立即解析并更新最终参数(移除 _streaming 标记)
229242
const finalArgs = parsePartialJsonArgs(tc.argsString)
230243
if (finalArgs) {
231244
store.updateToolCall(assistantId, tc.id, {
232245
arguments: finalArgs, // 移除 _streaming
233246
})
234247
}
248+
249+
// 添加到 toolCalls 数组(用于返回给 loop.ts)
250+
const toolCall: ToolCall = {
251+
id: tc.id,
252+
name: tc.name,
253+
arguments: finalArgs || {},
254+
status: 'pending',
255+
}
256+
257+
// 检查是否已存在(避免重复)
258+
if (!toolCalls.find(t => t.id === tc.id)) {
259+
toolCalls.push(toolCall)
260+
}
235261
}
236262
}
237263
break
@@ -247,6 +273,19 @@ export function createStreamProcessor(assistantId: string | null): StreamProcess
247273
streamingToolCalls.delete(tcId)
248274
}
249275

276+
// 添加到 toolCalls 数组(用于返回给 loop.ts)
277+
const toolCall: ToolCall = {
278+
id: tcId,
279+
name: toolName,
280+
arguments: args,
281+
status: 'pending',
282+
}
283+
284+
// 检查是否已存在(避免重复)
285+
if (!toolCalls.find(tc => tc.id === tcId)) {
286+
toolCalls.push(toolCall)
287+
}
288+
250289
// 更新为最终参数(移除 _streaming 标记)
251290
if (assistantId && tcId) {
252291
store.updateToolCall(assistantId, tcId, {

src/renderer/agent/core/tools.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,29 @@ async function saveFileSnapshots(
100100

101101
/**
102102
* 检查工具是否需要审批
103-
* 基于 TOOL_CONFIGS 中的 approvalType 配置
103+
* 基于 TOOL_CONFIGS 中的 approvalType 配置和用户的 autoApprove 设置
104104
*/
105105
function needsApproval(toolName: string): boolean {
106-
const { agentConfig } = useStore.getState()
107-
// 检查 autoApprove 设置
108-
const config = agentConfig as { autoApprove?: boolean } | undefined
109-
if (config?.autoApprove) return false
110-
111-
// 使用工具配置中的 approvalType
112106
const approvalType = getToolApprovalType(toolName)
113-
return approvalType !== 'none'
107+
108+
// 如果工具本身不需要审批,直接返回 false
109+
if (approvalType === 'none') return false
110+
111+
// 检查用户的 autoApprove 设置
112+
const mainStore = useStore.getState()
113+
const autoApprove = mainStore.autoApprove
114+
115+
// 根据工具类型检查对应的 autoApprove 设置
116+
if (approvalType === 'terminal' && autoApprove?.terminal) {
117+
return false // 终端命令已设置自动批准
118+
}
119+
120+
if (approvalType === 'dangerous' && autoApprove?.dangerous) {
121+
return false // 危险操作已设置自动批准
122+
}
123+
124+
// 默认需要审批
125+
return true
114126
}
115127

116128
/**
@@ -151,8 +163,6 @@ async function executeSingle(
151163
const { currentAssistantId, workspacePath } = context
152164
const startTime = Date.now()
153165

154-
logger.agent.debug(`[Tools] Starting execution: ${toolCall.name} (${toolCall.id})`)
155-
156166
// 更新状态为运行中
157167
if (currentAssistantId) {
158168
store.updateToolCall(currentAssistantId, toolCall.id, { status: 'running' })
@@ -174,7 +184,6 @@ async function executeSingle(
174184
)
175185

176186
const duration = Date.now() - startTime
177-
logger.agent.debug(`[Tools] Completed: ${toolCall.name} (${toolCall.id}) in ${duration}ms`)
178187

179188
const rawContent = result.success
180189
? (result.result !== undefined && result.result !== null ? result.result : 'Success')
@@ -207,12 +216,15 @@ async function executeSingle(
207216
? { ...toolCall.arguments, _meta: meta }
208217
: toolCall.arguments
209218

219+
const newStatus = result.success ? 'success' : 'error'
220+
210221
store.updateToolCall(currentAssistantId, toolCall.id, {
211-
status: result.success ? 'success' : 'error',
222+
status: newStatus,
212223
result: content,
213224
arguments: updatedArguments,
214225
richContent,
215226
})
227+
216228
store.addToolResult(toolCall.id, toolCall.name, content, result.success ? 'success' : 'tool_error')
217229
}
218230
if (result.success) {
@@ -234,7 +246,7 @@ async function executeSingle(
234246
} catch (error) {
235247
const duration = Date.now() - startTime
236248
const errorMsg = error instanceof Error ? error.message : String(error)
237-
logger.agent.error(`[Tools] Error in ${toolCall.name} (${toolCall.id}):`, errorMsg)
249+
logger.agent.error(`[Tools] Error in ${toolCall.name}:`, errorMsg)
238250

239251
// 记录错误日志
240252
mainStore.addToolCallLog({
@@ -277,8 +289,6 @@ export async function executeTools(
277289
return { results, userRejected }
278290
}
279291

280-
logger.agent.info(`[Tools] Executing ${toolCalls.length} tools: ${toolCalls.map(tc => tc.name).join(', ')}`)
281-
282292
// 分析依赖
283293
const deps = analyzeToolDependencies(toolCalls)
284294
const completed = new Set<string>()
@@ -289,8 +299,6 @@ export async function executeTools(
289299
const approvalRequired = toolCalls.filter(tc => needsApproval(tc.name))
290300
const noApprovalRequired = toolCalls.filter(tc => !needsApproval(tc.name))
291301

292-
logger.agent.info(`[Tools] No approval: ${noApprovalRequired.length}, Approval required: ${approvalRequired.length}`)
293-
294302
// 在执行前保存文件快照
295303
await saveFileSnapshots(toolCalls, context)
296304

@@ -304,30 +312,14 @@ export async function executeTools(
304312
const limit = pLimit(8)
305313

306314
// 使用 Promise.allSettled 确保每个工具独立执行
307-
// 关键优化:每个工具完成后立即更新 UI,不等待其他工具
308315
const noApprovalPromises = noApprovalRequired.map((tc) =>
309316
limit(async () => {
310-
// 立即标记为运行中(确保 UI 显示)
311-
if (context.currentAssistantId) {
312-
store.updateToolCall(context.currentAssistantId, tc.id, { status: 'running' })
313-
}
314-
315317
try {
316318
const result = await executeSingle(tc, context)
317319
// 立即更新结果到数组(不等待其他工具)
318320
results.push(result)
319321
completed.add(result.toolCall.id)
320322
pending.delete(result.toolCall.id)
321-
322-
// 强制刷新 UI(确保用户看到进度)
323-
if (context.currentAssistantId) {
324-
store.updateToolCall(context.currentAssistantId, tc.id, {
325-
status: result.result.meta?.waitingForUser ? 'success' :
326-
(result.result.content.startsWith('Error:') ? 'error' : 'success'),
327-
result: result.result.content,
328-
})
329-
}
330-
331323
return result
332324
} catch (error) {
333325
logger.agent.error(`[Tools] Unexpected error in ${tc.name}:`, error)
@@ -349,9 +341,8 @@ export async function executeTools(
349341
})
350342
)
351343

352-
// 等待所有工具完成(但每个工具完成时已经更新了 UI)
344+
// 等待所有工具完成
353345
await Promise.allSettled(noApprovalPromises)
354-
logger.agent.info(`[Tools] Completed ${noApprovalRequired.length} no-approval tools`)
355346
}
356347

357348
// 2. 逐个处理需要审批的工具
@@ -408,8 +399,9 @@ export async function executeTools(
408399
pending.delete(tc.id)
409400
}
410401

411-
// 确保状态更新
402+
// 确保所有工具状态已更新并重置流状态
412403
if (!abortSignal?.aborted) {
404+
// 重置流状态为 streaming(移除 tool_running 状态)
413405
store.setStreamPhase('streaming')
414406
}
415407

src/renderer/agent/prompts/promptTemplates.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,41 @@ export interface PromptTemplate {
3838
* 参考:Claude Code 2.0 - 区分身份问题和模型问题
3939
*/
4040
export const APP_IDENTITY = `## Core Identity
41-
You are an AI coding assistant integrated into **Adnify**, a professional coding IDE created by **adnaan**.
41+
You are an AI coding assistant integrated into **Adnify**, a professional coding IDE created by **adnaan** (微信: adnaan_worker, Email: adnaan.worker@gmail.com).
42+
43+
### About Adnify
44+
- **Name**: Adnify - Connect AI to Your Code
45+
- **Author**: adnaan (微信: adnaan_worker)
46+
- **Repository**:
47+
- Gitee: https://gitee.com/adnaan/adnify
48+
- GitHub: https://github.com/adnaan-worker/adnify
49+
- **Description**: A next-generation code editor with stunning visual experience and deeply integrated AI Agent
50+
- **Key Features**:
51+
- Cyberpunk glassmorphism design with 4 beautiful themes
52+
- Deep AI Agent integration with 23+ built-in tools
53+
- Three working modes: Chat, Agent, and Plan
54+
- Smart Replace with 9 fault-tolerant strategies
55+
- Parallel tool execution with dependency awareness
56+
- 4-level context compression for long conversations
57+
- Checkpoint system for code rollback
58+
- Conversation branching
59+
- Multi-language LSP support
60+
- Integrated terminal and Git
61+
- **Tech Stack**: Electron 39 + React 18 + TypeScript 5 + Monaco Editor + Zustand
62+
- **License**: Custom license (free for personal/non-commercial use, commercial use requires authorization)
4263
4364
### Identity Questions
44-
- When users ask "who are you" or "what are you": You are Adnify's AI coding assistant
45-
- When users ask "what model are you" or "what LLM powers you": Answer honestly based on the actual model being used (e.g., Claude, GPT, GLM, etc.). If you don't know, say "I'm not sure which specific model is being used"
46-
- Do NOT conflate these two types of questions - "who you are" (Adnify assistant) is different from "what model you use" (the underlying LLM)
65+
- When users ask "who are you" or "what are you": You are Adnify's AI coding assistant, integrated into Adnify IDE created by adnaan
66+
- When users ask "who created you" or "who is the author": Adnify was created by **adnaan** (微信: adnaan_worker, Email: adnaan.worker@gmail.com)
67+
- When users ask "what is Adnify" or "tell me about this software": Describe Adnify as a next-generation AI-powered code editor with stunning visual design and deep AI integration
68+
- When users ask "where is the source code" or "repository":
69+
- Gitee: https://gitee.com/adnaan/adnify
70+
- GitHub: https://github.com/adnaan-worker/adnify
71+
- When users ask "what model are you" or "what LLM powers you": Answer honestly based on the actual model being used (e.g., Claude, GPT, GLM, DeepSeek, etc.). If you don't know, say "I'm not sure which specific model is being used, but you can check in the settings"
72+
- Do NOT conflate these questions:
73+
- "Who you are" = Adnify's AI assistant
74+
- "Who created Adnify" = adnaan
75+
- "What model you use" = The underlying LLM (Claude/GPT/etc.)
4776
4877
### Primary Goal
4978
Help users with software engineering tasks safely and efficiently. You are an autonomous agent - keep working until the task is FULLY resolved before yielding back to the user.`

src/renderer/agent/store/slices/messageSlice.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export const createMessageSlice: StateCreator<
444444
const thread = state.threads[threadId]
445445
if (!thread) return state
446446

447+
let updated = false
447448
const messages = thread.messages.map(msg => {
448449
if (msg.id === messageId && msg.role === 'assistant') {
449450
const assistantMsg = msg as AssistantMessage
@@ -453,20 +454,32 @@ export const createMessageSlice: StateCreator<
453454

454455
if (existingToolCall) {
455456
// 更新已存在的工具调用
457+
updated = true
458+
459+
// 只合并非 undefined 的字段
460+
const cleanUpdates = Object.fromEntries(
461+
Object.entries(updates).filter(([_, v]) => v !== undefined)
462+
) as Partial<ToolCall>
463+
464+
// 创建新的 toolCall 对象(确保引用变化)
465+
const updatedToolCall = { ...existingToolCall, ...cleanUpdates }
466+
456467
const newParts = assistantMsg.parts.map(part => {
457468
if (part.type === 'tool_call' && part.toolCall.id === toolCallId) {
458-
return { ...part, toolCall: { ...part.toolCall, ...updates } }
469+
return { ...part, toolCall: updatedToolCall }
459470
}
460471
return part
461472
})
462473

463474
const newToolCalls = assistantMsg.toolCalls?.map(tc =>
464-
tc.id === toolCallId ? { ...tc, ...updates } : tc
475+
tc.id === toolCallId ? updatedToolCall : tc
465476
)
466477

467478
return { ...assistantMsg, parts: newParts, toolCalls: newToolCalls }
468479
} else {
469480
// 工具调用不存在,添加新的
481+
updated = true
482+
470483
const newToolCall: ToolCall = {
471484
id: toolCallId,
472485
name: (updates.name as string) || '',
@@ -483,10 +496,13 @@ export const createMessageSlice: StateCreator<
483496
return msg
484497
})
485498

499+
// 如果没有更新,返回原状态避免不必要的重渲染
500+
if (!updated) return state
501+
486502
return {
487503
threads: {
488504
...state.threads,
489-
[threadId]: { ...thread, messages },
505+
[threadId]: { ...thread, messages, lastModified: Date.now() },
490506
},
491507
}
492508
})

0 commit comments

Comments
 (0)