snow-ai 0.3.27 → 0.3.29

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.
@@ -18,27 +18,30 @@ export function convertSessionMessagesToUI(sessionMessages) {
18
18
  // Handle sub-agent internal tool call messages
19
19
  if (msg.subAgentInternal && msg.role === 'assistant' && msg.tool_calls) {
20
20
  for (const toolCall of msg.tool_calls) {
21
- const toolDisplay = formatToolCallMessage(toolCall);
22
- let toolArgs;
23
- try {
24
- toolArgs = JSON.parse(toolCall.function.arguments);
25
- }
26
- catch (e) {
27
- toolArgs = {};
21
+ // 只有耗时工具才创建"进行中"消息
22
+ if (isToolNeedTwoStepDisplay(toolCall.function.name)) {
23
+ const toolDisplay = formatToolCallMessage(toolCall);
24
+ let toolArgs;
25
+ try {
26
+ toolArgs = JSON.parse(toolCall.function.arguments);
27
+ }
28
+ catch (e) {
29
+ toolArgs = {};
30
+ }
31
+ uiMessages.push({
32
+ role: 'subagent',
33
+ content: `\x1b[38;2;184;122;206m⚇⚡ ${toolDisplay.toolName}\x1b[0m`,
34
+ streaming: false,
35
+ toolCall: {
36
+ name: toolCall.function.name,
37
+ arguments: toolArgs,
38
+ },
39
+ toolDisplay,
40
+ toolCallId: toolCall.id,
41
+ toolPending: false,
42
+ subAgentInternal: true,
43
+ });
28
44
  }
29
- uiMessages.push({
30
- role: 'subagent',
31
- content: `\x1b[38;2;184;122;206m⚇⚡ ${toolDisplay.toolName}\x1b[0m`,
32
- streaming: false,
33
- toolCall: {
34
- name: toolCall.function.name,
35
- arguments: toolArgs,
36
- },
37
- toolDisplay,
38
- toolCallId: toolCall.id,
39
- toolPending: false,
40
- subAgentInternal: true,
41
- });
42
45
  processedToolCalls.add(toolCall.id);
43
46
  }
44
47
  continue;
@@ -103,6 +106,12 @@ export function convertSessionMessagesToUI(sessionMessages) {
103
106
  msg.tool_calls &&
104
107
  msg.tool_calls.length > 0 &&
105
108
  !msg.subAgentInternal) {
109
+ // Generate parallel group ID for non-time-consuming tools
110
+ const hasMultipleTools = msg.tool_calls.length > 1;
111
+ const hasNonTimeConsumingTool = msg.tool_calls.some(tc => !isToolNeedTwoStepDisplay(tc.function.name));
112
+ const parallelGroupId = hasMultipleTools && hasNonTimeConsumingTool
113
+ ? `parallel-${i}-${Math.random()}`
114
+ : undefined;
106
115
  for (const toolCall of msg.tool_calls) {
107
116
  // Skip if already processed
108
117
  if (processedToolCalls.has(toolCall.id))
@@ -130,7 +139,15 @@ export function convertSessionMessagesToUI(sessionMessages) {
130
139
  toolDisplay,
131
140
  });
132
141
  }
133
- processedToolCalls.add(toolCall.id);
142
+ // Store parallel group info for this tool call
143
+ if (parallelGroupId && !needTwoSteps) {
144
+ processedToolCalls.add(toolCall.id);
145
+ // Mark this tool call with parallel group (will be used when processing tool results)
146
+ toolCall.parallelGroupId = parallelGroupId;
147
+ }
148
+ else {
149
+ processedToolCalls.add(toolCall.id);
150
+ }
134
151
  }
135
152
  continue;
136
153
  }
@@ -219,6 +236,23 @@ export function convertSessionMessagesToUI(sessionMessages) {
219
236
  }
220
237
  }
221
238
  }
239
+ // Check if this tool result is part of a parallel group
240
+ let parallelGroupId;
241
+ for (let j = i - 1; j >= 0; j--) {
242
+ const prevMsg = sessionMessages[j];
243
+ if (!prevMsg)
244
+ continue;
245
+ if (prevMsg.role === 'assistant' &&
246
+ prevMsg.tool_calls &&
247
+ !prevMsg.subAgentInternal) {
248
+ const tc = prevMsg.tool_calls.find(t => t.id === msg.tool_call_id);
249
+ if (tc) {
250
+ parallelGroupId = tc.parallelGroupId;
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ const isNonTimeConsuming = !isToolNeedTwoStepDisplay(toolName);
222
256
  uiMessages.push({
223
257
  role: 'assistant',
224
258
  content: `${statusIcon} ${toolName}${statusText}`,
@@ -231,6 +265,19 @@ export function convertSessionMessagesToUI(sessionMessages) {
231
265
  }
232
266
  : undefined,
233
267
  terminalResult: terminalResultData,
268
+ // Add toolDisplay for non-time-consuming tools
269
+ toolDisplay: isNonTimeConsuming && !editDiffData
270
+ ? formatToolCallMessage({
271
+ id: msg.tool_call_id || '',
272
+ type: 'function',
273
+ function: {
274
+ name: toolName,
275
+ arguments: JSON.stringify(toolArgs),
276
+ },
277
+ })
278
+ : undefined,
279
+ // Mark parallel group for non-time-consuming tools
280
+ parallelGroup: isNonTimeConsuming && parallelGroupId ? parallelGroupId : undefined,
234
281
  });
235
282
  continue;
236
283
  }
@@ -2,6 +2,7 @@ export interface SubAgent {
2
2
  id: string;
3
3
  name: string;
4
4
  description: string;
5
+ role?: string;
5
6
  tools: string[];
6
7
  createdAt: string;
7
8
  updatedAt: string;
@@ -20,13 +21,14 @@ export declare function getSubAgent(id: string): SubAgent | null;
20
21
  /**
21
22
  * Create a new sub-agent
22
23
  */
23
- export declare function createSubAgent(name: string, description: string, tools: string[]): SubAgent;
24
+ export declare function createSubAgent(name: string, description: string, tools: string[], role?: string): SubAgent;
24
25
  /**
25
26
  * Update an existing sub-agent
26
27
  */
27
28
  export declare function updateSubAgent(id: string, updates: {
28
29
  name?: string;
29
30
  description?: string;
31
+ role?: string;
30
32
  tools?: string[];
31
33
  }): SubAgent | null;
32
34
  /**
@@ -53,13 +53,14 @@ function saveSubAgents(agents) {
53
53
  /**
54
54
  * Create a new sub-agent
55
55
  */
56
- export function createSubAgent(name, description, tools) {
56
+ export function createSubAgent(name, description, tools, role) {
57
57
  const agents = getSubAgents();
58
58
  const now = new Date().toISOString();
59
59
  const newAgent = {
60
60
  id: generateId(),
61
61
  name,
62
62
  description,
63
+ role,
63
64
  tools,
64
65
  createdAt: now,
65
66
  updatedAt: now,
@@ -85,6 +86,7 @@ export function updateSubAgent(id, updates) {
85
86
  id: existingAgent.id,
86
87
  name: updates.name ?? existingAgent.name,
87
88
  description: updates.description ?? existingAgent.description,
89
+ role: updates.role ?? existingAgent.role,
88
90
  tools: updates.tools ?? existingAgent.tools,
89
91
  createdAt: existingAgent.createdAt,
90
92
  updatedAt: new Date().toISOString(),
@@ -50,10 +50,15 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
50
50
  };
51
51
  }
52
52
  // Build conversation history for sub-agent
53
+ // Append role to prompt if configured
54
+ let finalPrompt = prompt;
55
+ if (agent.role) {
56
+ finalPrompt = `${prompt}\n\n${agent.role}`;
57
+ }
53
58
  const messages = [
54
59
  {
55
60
  role: 'user',
56
- content: prompt,
61
+ content: finalPrompt,
57
62
  },
58
63
  ];
59
64
  // Stream sub-agent execution
@@ -27,6 +27,8 @@ export interface AddToAlwaysApprovedCallback {
27
27
  */
28
28
  export declare function executeToolCall(toolCall: ToolCall, abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<ToolResult>;
29
29
  /**
30
- * Execute multiple tool calls in parallel
30
+ * Execute multiple tool calls with intelligent sequencing
31
+ * - Tools modifying the same resource execute sequentially
32
+ * - Independent tools execute in parallel
31
33
  */
32
34
  export declare function executeToolCalls(toolCalls: ToolCall[], abortSignal?: AbortSignal, onTokenUpdate?: (tokenCount: number) => void, onSubAgentMessage?: SubAgentMessageCallback, requestToolConfirmation?: ToolConfirmationCallback, isToolAutoApproved?: ToolApprovalChecker, yoloMode?: boolean, addToAlwaysApproved?: AddToAlwaysApprovedCallback): Promise<ToolResult[]>;
@@ -63,8 +63,96 @@ export async function executeToolCall(toolCall, abortSignal, onTokenUpdate, onSu
63
63
  }
64
64
  }
65
65
  /**
66
- * Execute multiple tool calls in parallel
66
+ * Categorize tools by their resource type for proper execution sequencing
67
+ */
68
+ function getToolResourceType(toolName) {
69
+ // TODO tools all modify the same TODO file - must be sequential
70
+ if (toolName === 'todo-create' ||
71
+ toolName === 'todo-update' ||
72
+ toolName === 'todo-add' ||
73
+ toolName === 'todo-delete') {
74
+ return 'todo-state';
75
+ }
76
+ // Terminal commands must be sequential to avoid race conditions
77
+ // (e.g., npm install -> npm build, port conflicts, file locks)
78
+ if (toolName === 'terminal-execute') {
79
+ return 'terminal-execution';
80
+ }
81
+ // Each file is a separate resource
82
+ if (toolName === 'filesystem-edit' ||
83
+ toolName === 'filesystem-edit_search' ||
84
+ toolName === 'filesystem-create' ||
85
+ toolName === 'filesystem-delete') {
86
+ return 'filesystem'; // Will be further refined by file path
87
+ }
88
+ // Other tools are independent
89
+ return 'independent';
90
+ }
91
+ /**
92
+ * Get resource identifier for a tool call
93
+ * Tools modifying the same resource will have the same identifier
94
+ */
95
+ function getResourceIdentifier(toolCall) {
96
+ const toolName = toolCall.function.name;
97
+ const resourceType = getToolResourceType(toolName);
98
+ if (resourceType === 'todo-state') {
99
+ return 'todo-state'; // All TODO operations share same resource
100
+ }
101
+ if (resourceType === 'terminal-execution') {
102
+ return 'terminal-execution'; // All terminal commands share same execution context
103
+ }
104
+ if (resourceType === 'filesystem') {
105
+ try {
106
+ const args = JSON.parse(toolCall.function.arguments);
107
+ // Support both single file and array of files
108
+ const filePath = args.filePath;
109
+ if (typeof filePath === 'string') {
110
+ return `filesystem:${filePath}`;
111
+ }
112
+ else if (Array.isArray(filePath)) {
113
+ // For batch operations, treat as independent (already handling multiple files)
114
+ return `filesystem-batch:${toolCall.id}`;
115
+ }
116
+ }
117
+ catch {
118
+ // Parsing error, treat as independent
119
+ }
120
+ }
121
+ // Each independent tool gets its own unique identifier
122
+ return `independent:${toolCall.id}`;
123
+ }
124
+ /**
125
+ * Execute multiple tool calls with intelligent sequencing
126
+ * - Tools modifying the same resource execute sequentially
127
+ * - Independent tools execute in parallel
67
128
  */
68
129
  export async function executeToolCalls(toolCalls, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved) {
69
- return Promise.all(toolCalls.map(tc => executeToolCall(tc, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved)));
130
+ // Group tool calls by their resource identifier
131
+ const resourceGroups = new Map();
132
+ for (const toolCall of toolCalls) {
133
+ const resourceId = getResourceIdentifier(toolCall);
134
+ const group = resourceGroups.get(resourceId) || [];
135
+ group.push(toolCall);
136
+ resourceGroups.set(resourceId, group);
137
+ }
138
+ // Execute each resource group sequentially, but execute different groups in parallel
139
+ const results = await Promise.all(Array.from(resourceGroups.values()).map(async (group) => {
140
+ // Within the same resource group, execute sequentially
141
+ const groupResults = [];
142
+ for (const toolCall of group) {
143
+ const result = await executeToolCall(toolCall, abortSignal, onTokenUpdate, onSubAgentMessage, requestToolConfirmation, isToolAutoApproved, yoloMode, addToAlwaysApproved);
144
+ groupResults.push(result);
145
+ }
146
+ return groupResults;
147
+ }));
148
+ // Flatten results and restore original order
149
+ const flatResults = results.flat();
150
+ const resultMap = new Map(flatResults.map(r => [r.tool_call_id, r]));
151
+ return toolCalls.map(tc => {
152
+ const result = resultMap.get(tc.id);
153
+ if (!result) {
154
+ throw new Error(`Result not found for tool call ${tc.id}`);
155
+ }
156
+ return result;
157
+ });
70
158
  }
@@ -30,7 +30,14 @@ declare class VSCodeConnectionManager {
30
30
  private editorContext;
31
31
  private listeners;
32
32
  private currentWorkingDirectory;
33
+ private connectingPromise;
34
+ private connectionTimeout;
35
+ private readonly CONNECTION_TIMEOUT;
33
36
  start(): Promise<void>;
37
+ /**
38
+ * Clean up connection state and resources
39
+ */
40
+ private cleanupConnection;
34
41
  /**
35
42
  * Normalize path for cross-platform compatibility
36
43
  * - Converts Windows backslashes to forward slashes
@@ -89,15 +89,44 @@ class VSCodeConnectionManager {
89
89
  writable: true,
90
90
  value: process.cwd()
91
91
  });
92
+ // Connection state management
93
+ Object.defineProperty(this, "connectingPromise", {
94
+ enumerable: true,
95
+ configurable: true,
96
+ writable: true,
97
+ value: null
98
+ });
99
+ Object.defineProperty(this, "connectionTimeout", {
100
+ enumerable: true,
101
+ configurable: true,
102
+ writable: true,
103
+ value: null
104
+ });
105
+ Object.defineProperty(this, "CONNECTION_TIMEOUT", {
106
+ enumerable: true,
107
+ configurable: true,
108
+ writable: true,
109
+ value: 10000
110
+ }); // 10 seconds timeout for initial connection
92
111
  }
93
112
  async start() {
94
113
  // If already connected, just return success
95
114
  if (this.client?.readyState === WebSocket.OPEN) {
96
115
  return Promise.resolve();
97
116
  }
117
+ // If already connecting, return the existing promise to avoid duplicate connections
118
+ if (this.connectingPromise) {
119
+ return this.connectingPromise;
120
+ }
98
121
  // Try to find the correct port for this workspace
99
122
  const targetPort = this.findPortForWorkspace();
100
- return new Promise((resolve, reject) => {
123
+ // Create a new connection promise and store it
124
+ this.connectingPromise = new Promise((resolve, reject) => {
125
+ // Set connection timeout
126
+ this.connectionTimeout = setTimeout(() => {
127
+ this.cleanupConnection();
128
+ reject(new Error('Connection timeout after 10 seconds'));
129
+ }, this.CONNECTION_TIMEOUT);
101
130
  const tryConnect = (port) => {
102
131
  // Check both VSCode and JetBrains port ranges
103
132
  if (port > this.VSCODE_MAX_PORT && port < this.JETBRAINS_BASE_PORT) {
@@ -106,6 +135,7 @@ class VSCodeConnectionManager {
106
135
  return;
107
136
  }
108
137
  if (port > this.JETBRAINS_MAX_PORT) {
138
+ this.cleanupConnection();
109
139
  reject(new Error(`Failed to connect: no IDE server found on ports ${this.VSCODE_BASE_PORT}-${this.VSCODE_MAX_PORT} or ${this.JETBRAINS_BASE_PORT}-${this.JETBRAINS_MAX_PORT}`));
110
140
  return;
111
141
  }
@@ -115,6 +145,12 @@ class VSCodeConnectionManager {
115
145
  // Reset reconnect attempts on successful connection
116
146
  this.reconnectAttempts = 0;
117
147
  this.port = port;
148
+ // Clear connection state
149
+ if (this.connectionTimeout) {
150
+ clearTimeout(this.connectionTimeout);
151
+ this.connectionTimeout = null;
152
+ }
153
+ this.connectingPromise = null;
118
154
  resolve();
119
155
  });
120
156
  this.client.on('message', message => {
@@ -148,6 +184,34 @@ class VSCodeConnectionManager {
148
184
  };
149
185
  tryConnect(targetPort);
150
186
  });
187
+ // Return the promise and clean up state when it completes or fails
188
+ return this.connectingPromise.finally(() => {
189
+ this.connectingPromise = null;
190
+ if (this.connectionTimeout) {
191
+ clearTimeout(this.connectionTimeout);
192
+ this.connectionTimeout = null;
193
+ }
194
+ });
195
+ }
196
+ /**
197
+ * Clean up connection state and resources
198
+ */
199
+ cleanupConnection() {
200
+ this.connectingPromise = null;
201
+ if (this.connectionTimeout) {
202
+ clearTimeout(this.connectionTimeout);
203
+ this.connectionTimeout = null;
204
+ }
205
+ if (this.client) {
206
+ try {
207
+ this.client.removeAllListeners();
208
+ this.client.close();
209
+ }
210
+ catch (error) {
211
+ // Ignore errors during cleanup
212
+ }
213
+ this.client = null;
214
+ }
151
215
  }
152
216
  /**
153
217
  * Normalize path for cross-platform compatibility
@@ -228,8 +292,21 @@ class VSCodeConnectionManager {
228
292
  clearTimeout(this.reconnectTimer);
229
293
  this.reconnectTimer = null;
230
294
  }
295
+ // Clear connection timeout
296
+ if (this.connectionTimeout) {
297
+ clearTimeout(this.connectionTimeout);
298
+ this.connectionTimeout = null;
299
+ }
300
+ // Clear connecting promise
301
+ this.connectingPromise = null;
231
302
  if (this.client) {
232
- this.client.close();
303
+ try {
304
+ this.client.removeAllListeners();
305
+ this.client.close();
306
+ }
307
+ catch (error) {
308
+ // Ignore errors during cleanup
309
+ }
233
310
  this.client = null;
234
311
  }
235
312
  this.reconnectAttempts = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.27",
3
+ "version": "0.3.29",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {