wave-agent-sdk 0.0.2 → 0.0.4
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/dist/agent.d.ts +5 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +48 -4
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +4 -7
- package/dist/managers/messageManager.d.ts +8 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +26 -2
- package/dist/managers/skillManager.d.ts +4 -5
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +6 -82
- package/dist/managers/subagentManager.d.ts +96 -0
- package/dist/managers/subagentManager.d.ts.map +1 -0
- package/dist/managers/subagentManager.js +261 -0
- package/dist/managers/toolManager.d.ts +33 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +43 -5
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +40 -14
- package/dist/services/memory.js +2 -2
- package/dist/tools/grepTool.d.ts.map +1 -1
- package/dist/tools/grepTool.js +8 -6
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +36 -6
- package/dist/tools/skillTool.d.ts +8 -0
- package/dist/tools/skillTool.d.ts.map +1 -0
- package/dist/tools/skillTool.js +72 -0
- package/dist/tools/taskTool.d.ts +8 -0
- package/dist/tools/taskTool.d.ts.map +1 -0
- package/dist/tools/taskTool.js +109 -0
- package/dist/tools/todoWriteTool.d.ts +6 -0
- package/dist/tools/todoWriteTool.d.ts.map +1 -0
- package/dist/tools/todoWriteTool.js +203 -0
- package/dist/types.d.ts +8 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/fileFormat.d.ts +17 -0
- package/dist/utils/fileFormat.d.ts.map +1 -0
- package/dist/utils/fileFormat.js +35 -0
- package/dist/utils/messageOperations.d.ts +18 -0
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +43 -0
- package/dist/utils/subagentParser.d.ts +19 -0
- package/dist/utils/subagentParser.d.ts.map +1 -0
- package/dist/utils/subagentParser.js +159 -0
- package/package.json +1 -1
- package/src/agent.ts +55 -5
- package/src/index.ts +0 -1
- package/src/managers/aiManager.ts +5 -8
- package/src/managers/messageManager.ts +55 -1
- package/src/managers/skillManager.ts +7 -96
- package/src/managers/subagentManager.ts +368 -0
- package/src/managers/toolManager.ts +50 -5
- package/src/services/aiService.ts +44 -16
- package/src/services/memory.ts +2 -2
- package/src/tools/grepTool.ts +9 -6
- package/src/tools/readTool.ts +40 -6
- package/src/tools/skillTool.ts +82 -0
- package/src/tools/taskTool.ts +128 -0
- package/src/tools/todoWriteTool.ts +232 -0
- package/src/types.ts +10 -1
- package/src/utils/fileFormat.ts +40 -0
- package/src/utils/messageOperations.ts +80 -0
- package/src/utils/subagentParser.ts +223 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import type { SubagentConfiguration } from "../utils/subagentParser.js";
|
|
3
|
+
import type { Message, Logger, GatewayConfig, ModelConfig } from "../types.js";
|
|
4
|
+
import { AIManager } from "./aiManager.js";
|
|
5
|
+
import {
|
|
6
|
+
MessageManager,
|
|
7
|
+
type MessageManagerCallbacks,
|
|
8
|
+
} from "./messageManager.js";
|
|
9
|
+
import { ToolManager } from "./toolManager.js";
|
|
10
|
+
|
|
11
|
+
export interface SubagentInstance {
|
|
12
|
+
subagentId: string;
|
|
13
|
+
configuration: SubagentConfiguration;
|
|
14
|
+
aiManager: AIManager;
|
|
15
|
+
messageManager: MessageManager;
|
|
16
|
+
toolManager: ToolManager;
|
|
17
|
+
status: "initializing" | "active" | "completed" | "error" | "aborted";
|
|
18
|
+
taskDescription: string;
|
|
19
|
+
messages: Message[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubagentManagerOptions {
|
|
23
|
+
workdir: string;
|
|
24
|
+
parentToolManager: ToolManager;
|
|
25
|
+
parentMessageManager: MessageManager;
|
|
26
|
+
logger?: Logger;
|
|
27
|
+
gatewayConfig: GatewayConfig;
|
|
28
|
+
modelConfig: ModelConfig;
|
|
29
|
+
tokenLimit: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SubagentManager {
|
|
33
|
+
private instances = new Map<string, SubagentInstance>();
|
|
34
|
+
private cachedConfigurations: SubagentConfiguration[] | null = null;
|
|
35
|
+
|
|
36
|
+
private workdir: string;
|
|
37
|
+
private parentToolManager: ToolManager;
|
|
38
|
+
private parentMessageManager: MessageManager;
|
|
39
|
+
private logger?: Logger;
|
|
40
|
+
private gatewayConfig: GatewayConfig;
|
|
41
|
+
private modelConfig: ModelConfig;
|
|
42
|
+
private tokenLimit: number;
|
|
43
|
+
|
|
44
|
+
constructor(options: SubagentManagerOptions) {
|
|
45
|
+
this.workdir = options.workdir;
|
|
46
|
+
this.parentToolManager = options.parentToolManager;
|
|
47
|
+
this.parentMessageManager = options.parentMessageManager;
|
|
48
|
+
this.logger = options.logger;
|
|
49
|
+
this.gatewayConfig = options.gatewayConfig;
|
|
50
|
+
this.modelConfig = options.modelConfig;
|
|
51
|
+
this.tokenLimit = options.tokenLimit;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the SubagentManager by loading and caching configurations
|
|
56
|
+
*/
|
|
57
|
+
async initialize(): Promise<void> {
|
|
58
|
+
await this.loadConfigurations();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load all available subagent configurations and cache them
|
|
63
|
+
*/
|
|
64
|
+
async loadConfigurations(): Promise<SubagentConfiguration[]> {
|
|
65
|
+
if (this.cachedConfigurations === null) {
|
|
66
|
+
const { loadSubagentConfigurations } = await import(
|
|
67
|
+
"../utils/subagentParser.js"
|
|
68
|
+
);
|
|
69
|
+
this.cachedConfigurations = await loadSubagentConfigurations(
|
|
70
|
+
this.workdir,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return this.cachedConfigurations;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get cached configurations synchronously (must call loadConfigurations first)
|
|
78
|
+
*/
|
|
79
|
+
getConfigurations(): SubagentConfiguration[] {
|
|
80
|
+
if (this.cachedConfigurations === null) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"SubagentManager not initialized. Call loadConfigurations() first.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return this.cachedConfigurations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find subagent by exact name match
|
|
90
|
+
*/
|
|
91
|
+
async findSubagent(name: string) {
|
|
92
|
+
const { findSubagentByName } = await import("../utils/subagentParser.js");
|
|
93
|
+
return findSubagentByName(name, this.workdir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new subagent instance with isolated managers
|
|
98
|
+
*/
|
|
99
|
+
async createInstance(
|
|
100
|
+
configuration: SubagentConfiguration,
|
|
101
|
+
taskDescription: string,
|
|
102
|
+
): Promise<SubagentInstance> {
|
|
103
|
+
if (
|
|
104
|
+
!this.parentToolManager ||
|
|
105
|
+
!this.gatewayConfig ||
|
|
106
|
+
!this.modelConfig ||
|
|
107
|
+
!this.tokenLimit
|
|
108
|
+
) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"SubagentManager not properly initialized - call initialize() first",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const subagentId = randomUUID();
|
|
115
|
+
|
|
116
|
+
// Create isolated MessageManager for the subagent
|
|
117
|
+
const subagentCallbacks: MessageManagerCallbacks = {
|
|
118
|
+
// These callbacks will be handled by the parent agent
|
|
119
|
+
onMessagesChange: (messages: Message[]) => {
|
|
120
|
+
const instance = this.instances.get(subagentId);
|
|
121
|
+
if (instance) {
|
|
122
|
+
instance.messages = messages;
|
|
123
|
+
// Update parent's subagent block with latest messages
|
|
124
|
+
this.parentMessageManager.updateSubagentBlock(subagentId, {
|
|
125
|
+
messages: messages,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const messageManager = new MessageManager({
|
|
132
|
+
callbacks: subagentCallbacks,
|
|
133
|
+
workdir: this.workdir,
|
|
134
|
+
logger: this.logger,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Use the parent tool manager directly - tool restrictions will be handled by allowedTools parameter
|
|
138
|
+
const toolManager = this.parentToolManager;
|
|
139
|
+
|
|
140
|
+
// Determine model to use
|
|
141
|
+
const modelToUse =
|
|
142
|
+
configuration.model && configuration.model !== "inherit"
|
|
143
|
+
? configuration.model
|
|
144
|
+
: this.modelConfig.agentModel;
|
|
145
|
+
|
|
146
|
+
// Create isolated AIManager for the subagent
|
|
147
|
+
const aiManager = new AIManager({
|
|
148
|
+
messageManager,
|
|
149
|
+
toolManager,
|
|
150
|
+
logger: this.logger,
|
|
151
|
+
workdir: this.workdir,
|
|
152
|
+
systemPrompt: configuration.systemPrompt,
|
|
153
|
+
gatewayConfig: this.gatewayConfig,
|
|
154
|
+
modelConfig: {
|
|
155
|
+
...this.modelConfig,
|
|
156
|
+
agentModel: modelToUse,
|
|
157
|
+
},
|
|
158
|
+
tokenLimit: this.tokenLimit,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const instance: SubagentInstance = {
|
|
162
|
+
subagentId,
|
|
163
|
+
configuration,
|
|
164
|
+
aiManager,
|
|
165
|
+
messageManager,
|
|
166
|
+
toolManager,
|
|
167
|
+
status: "initializing",
|
|
168
|
+
taskDescription,
|
|
169
|
+
messages: [],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.instances.set(subagentId, instance);
|
|
173
|
+
|
|
174
|
+
// Create subagent block in parent message manager
|
|
175
|
+
this.parentMessageManager.addSubagentBlock(
|
|
176
|
+
subagentId,
|
|
177
|
+
configuration.name,
|
|
178
|
+
"active",
|
|
179
|
+
[],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return instance;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Execute task using subagent instance
|
|
187
|
+
*
|
|
188
|
+
* IMPORTANT: This method automatically filters out the Task tool from allowedTools
|
|
189
|
+
* to prevent subagents from spawning other subagents (infinite recursion protection)
|
|
190
|
+
*/
|
|
191
|
+
async executeTask(
|
|
192
|
+
instance: SubagentInstance,
|
|
193
|
+
prompt: string,
|
|
194
|
+
): Promise<string> {
|
|
195
|
+
try {
|
|
196
|
+
// Set status to active and update parent
|
|
197
|
+
this.updateInstanceStatus(instance.subagentId, "active");
|
|
198
|
+
this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
|
|
199
|
+
status: "active",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Add the user's prompt as a message
|
|
203
|
+
instance.messageManager.addUserMessage(prompt);
|
|
204
|
+
|
|
205
|
+
// Create allowed tools list - always exclude Task tool to prevent subagent recursion
|
|
206
|
+
let allowedTools = instance.configuration.tools;
|
|
207
|
+
|
|
208
|
+
// Always filter out the Task tool to prevent subagents from creating sub-subagents
|
|
209
|
+
if (allowedTools) {
|
|
210
|
+
allowedTools = allowedTools.filter((tool) => tool !== "Task");
|
|
211
|
+
} else {
|
|
212
|
+
// If no tools specified, get all tools except Task
|
|
213
|
+
const allTools = instance.toolManager.list().map((tool) => tool.name);
|
|
214
|
+
allowedTools = allTools.filter((tool) => tool !== "Task");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Execute the AI request with tool restrictions
|
|
218
|
+
await instance.aiManager.sendAIMessage({
|
|
219
|
+
allowedTools,
|
|
220
|
+
model:
|
|
221
|
+
instance.configuration.model !== "inherit"
|
|
222
|
+
? instance.configuration.model
|
|
223
|
+
: undefined,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Get the latest messages to extract the response
|
|
227
|
+
const messages = instance.messageManager.getMessages();
|
|
228
|
+
const lastAssistantMessage = messages
|
|
229
|
+
.filter((msg) => msg.role === "assistant")
|
|
230
|
+
.pop();
|
|
231
|
+
|
|
232
|
+
if (!lastAssistantMessage) {
|
|
233
|
+
throw new Error("No response from subagent");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Extract text content from the last assistant message
|
|
237
|
+
const textBlocks = lastAssistantMessage.blocks.filter(
|
|
238
|
+
(block) => block.type === "text",
|
|
239
|
+
);
|
|
240
|
+
const response = textBlocks.map((block) => block.content).join("\n");
|
|
241
|
+
|
|
242
|
+
// Update status to completed and update parent with final messages
|
|
243
|
+
this.updateInstanceStatus(instance.subagentId, "completed");
|
|
244
|
+
this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
|
|
245
|
+
status: "completed",
|
|
246
|
+
messages: messages,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return response || "Task completed with no text response";
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.updateInstanceStatus(instance.subagentId, "error");
|
|
252
|
+
this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
|
|
253
|
+
status: "error",
|
|
254
|
+
});
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get instance by subagent ID
|
|
261
|
+
*/
|
|
262
|
+
getInstance(subagentId: string): SubagentInstance | null {
|
|
263
|
+
return this.instances.get(subagentId) || null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Update instance status
|
|
268
|
+
*/
|
|
269
|
+
updateInstanceStatus(
|
|
270
|
+
subagentId: string,
|
|
271
|
+
status: SubagentInstance["status"],
|
|
272
|
+
): void {
|
|
273
|
+
const instance = this.instances.get(subagentId);
|
|
274
|
+
if (instance) {
|
|
275
|
+
instance.status = status;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Add message to instance
|
|
281
|
+
*/
|
|
282
|
+
addMessageToInstance(subagentId: string, message: Message): void {
|
|
283
|
+
const instance = this.instances.get(subagentId);
|
|
284
|
+
if (instance) {
|
|
285
|
+
instance.messages.push(message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Abort a running subagent instance
|
|
291
|
+
*/
|
|
292
|
+
abortInstance(subagentId: string): boolean {
|
|
293
|
+
const instance = this.instances.get(subagentId);
|
|
294
|
+
if (!instance) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Only abort active or initializing instances
|
|
299
|
+
if (instance.status !== "active" && instance.status !== "initializing") {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
// Abort the AI manager operations
|
|
305
|
+
instance.aiManager.abortAIMessage();
|
|
306
|
+
|
|
307
|
+
// Update status
|
|
308
|
+
this.updateInstanceStatus(subagentId, "aborted");
|
|
309
|
+
this.parentMessageManager.updateSubagentBlock(subagentId, {
|
|
310
|
+
status: "aborted",
|
|
311
|
+
messages: instance.messages,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.logger?.info(`Aborted subagent instance: ${subagentId}`);
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.logger?.error(
|
|
318
|
+
`Failed to abort subagent instance ${subagentId}:`,
|
|
319
|
+
error,
|
|
320
|
+
);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Abort all active subagent instances
|
|
327
|
+
*/
|
|
328
|
+
abortAllInstances(): void {
|
|
329
|
+
const activeInstances = this.getActiveInstances();
|
|
330
|
+
for (const instance of activeInstances) {
|
|
331
|
+
this.abortInstance(instance.subagentId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Clean up completed, errored, or aborted instances
|
|
337
|
+
*/
|
|
338
|
+
cleanupInstance(subagentId: string): void {
|
|
339
|
+
const instance = this.instances.get(subagentId);
|
|
340
|
+
if (
|
|
341
|
+
instance &&
|
|
342
|
+
(instance.status === "completed" ||
|
|
343
|
+
instance.status === "error" ||
|
|
344
|
+
instance.status === "aborted")
|
|
345
|
+
) {
|
|
346
|
+
this.instances.delete(subagentId);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get all active instances
|
|
352
|
+
*/
|
|
353
|
+
getActiveInstances(): SubagentInstance[] {
|
|
354
|
+
return Array.from(this.instances.values()).filter(
|
|
355
|
+
(instance) =>
|
|
356
|
+
instance.status === "active" || instance.status === "initializing",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Clean up all instances (for session end)
|
|
362
|
+
*/
|
|
363
|
+
cleanup(): void {
|
|
364
|
+
// Abort all active instances before cleanup
|
|
365
|
+
this.abortAllInstances();
|
|
366
|
+
this.instances.clear();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -9,10 +9,14 @@ import { globTool } from "../tools/globTool.js";
|
|
|
9
9
|
import { grepTool } from "../tools/grepTool.js";
|
|
10
10
|
import { lsTool } from "../tools/lsTool.js";
|
|
11
11
|
import { readTool } from "../tools/readTool.js";
|
|
12
|
-
import {
|
|
12
|
+
import { todoWriteTool } from "../tools/todoWriteTool.js";
|
|
13
|
+
import { createTaskTool } from "../tools/taskTool.js";
|
|
14
|
+
import { createSkillTool } from "../tools/skillTool.js";
|
|
13
15
|
import { McpManager } from "./mcpManager.js";
|
|
14
16
|
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
15
17
|
import type { Logger } from "../types.js";
|
|
18
|
+
import type { SubagentManager } from "./subagentManager.js";
|
|
19
|
+
import type { SkillManager } from "./skillManager.js";
|
|
16
20
|
|
|
17
21
|
export interface ToolManagerOptions {
|
|
18
22
|
mcpManager: McpManager;
|
|
@@ -30,12 +34,42 @@ class ToolManager {
|
|
|
30
34
|
constructor(options: ToolManagerOptions) {
|
|
31
35
|
this.mcpManager = options.mcpManager;
|
|
32
36
|
this.logger = options.logger;
|
|
37
|
+
}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Register a new tool
|
|
41
|
+
*/
|
|
42
|
+
public register(tool: ToolPlugin): void {
|
|
43
|
+
this.tools.set(tool.name, tool);
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Initialize built-in tools. Can be called with dependencies for tools that require them.
|
|
48
|
+
*
|
|
49
|
+
* This method can be called multiple times safely. When called without dependencies,
|
|
50
|
+
* it registers basic tools (Bash, Read, Write, TodoWrite, etc.). When called with
|
|
51
|
+
* dependencies, it also registers tools that require managers (Task, Skill).
|
|
52
|
+
*
|
|
53
|
+
* @param deps Optional dependencies for advanced tools
|
|
54
|
+
* @param deps.subagentManager SubagentManager instance for Task tool
|
|
55
|
+
* @param deps.skillManager SkillManager instance for Skill tool
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Initialize basic tools only
|
|
60
|
+
* toolManager.initializeBuiltInTools();
|
|
61
|
+
*
|
|
62
|
+
* // Initialize all tools including those requiring dependencies
|
|
63
|
+
* toolManager.initializeBuiltInTools({
|
|
64
|
+
* subagentManager: mySubagentManager,
|
|
65
|
+
* skillManager: mySkillManager
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
public initializeBuiltInTools(deps?: {
|
|
70
|
+
subagentManager?: SubagentManager;
|
|
71
|
+
skillManager?: SkillManager;
|
|
72
|
+
}): void {
|
|
39
73
|
const builtInTools = [
|
|
40
74
|
bashTool,
|
|
41
75
|
bashOutputTool,
|
|
@@ -48,12 +82,23 @@ class ToolManager {
|
|
|
48
82
|
grepTool,
|
|
49
83
|
lsTool,
|
|
50
84
|
readTool,
|
|
51
|
-
|
|
85
|
+
todoWriteTool,
|
|
52
86
|
];
|
|
53
87
|
|
|
54
88
|
for (const tool of builtInTools) {
|
|
55
89
|
this.tools.set(tool.name, tool);
|
|
56
90
|
}
|
|
91
|
+
|
|
92
|
+
// Register tools that require dependencies
|
|
93
|
+
if (deps?.subagentManager) {
|
|
94
|
+
const taskTool = createTaskTool(deps.subagentManager);
|
|
95
|
+
this.tools.set(taskTool.name, taskTool);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (deps?.skillManager) {
|
|
99
|
+
const skillTool = createSkillTool(deps.skillManager);
|
|
100
|
+
this.tools.set(skillTool.name, skillTool);
|
|
101
|
+
}
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
async execute(
|
|
@@ -6,6 +6,31 @@ import {
|
|
|
6
6
|
ChatCompletionFunctionTool,
|
|
7
7
|
} from "openai/resources.js";
|
|
8
8
|
import type { GatewayConfig, ModelConfig } from "../types.js";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a directory is a git repository
|
|
15
|
+
* @param dirPath Directory path to check
|
|
16
|
+
* @returns "Yes" if it's a git repo, "No" otherwise
|
|
17
|
+
*/
|
|
18
|
+
function isGitRepository(dirPath: string): string {
|
|
19
|
+
try {
|
|
20
|
+
// Check if .git directory exists in current directory or any parent directory
|
|
21
|
+
let currentPath = path.resolve(dirPath);
|
|
22
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
23
|
+
const gitPath = path.join(currentPath, ".git");
|
|
24
|
+
if (fs.existsSync(gitPath)) {
|
|
25
|
+
return "Yes";
|
|
26
|
+
}
|
|
27
|
+
currentPath = path.dirname(currentPath);
|
|
28
|
+
}
|
|
29
|
+
return "No";
|
|
30
|
+
} catch {
|
|
31
|
+
return "No";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
9
34
|
|
|
10
35
|
/**
|
|
11
36
|
* OpenAI model configuration type, based on OpenAI parameters but excluding messages
|
|
@@ -49,7 +74,7 @@ export interface CallAgentOptions {
|
|
|
49
74
|
messages: ChatCompletionMessageParam[];
|
|
50
75
|
sessionId?: string;
|
|
51
76
|
abortSignal?: AbortSignal;
|
|
52
|
-
memory?: string; // Memory content parameter, content read from
|
|
77
|
+
memory?: string; // Memory content parameter, content read from AGENTS.md
|
|
53
78
|
workdir: string; // Current working directory
|
|
54
79
|
tools?: ChatCompletionFunctionTool[]; // Tool configuration
|
|
55
80
|
model?: string; // Custom model
|
|
@@ -89,23 +114,26 @@ export async function callAgent(
|
|
|
89
114
|
});
|
|
90
115
|
|
|
91
116
|
// Build system prompt content
|
|
92
|
-
let systemContent
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
${workdir}
|
|
117
|
+
let systemContent =
|
|
118
|
+
systemPrompt ||
|
|
119
|
+
`You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
|
|
120
|
+
|
|
121
|
+
// Always add environment information
|
|
122
|
+
systemContent += `
|
|
123
|
+
|
|
124
|
+
Here is useful information about the environment you are running in:
|
|
125
|
+
<env>
|
|
126
|
+
Working directory: ${workdir}
|
|
127
|
+
Is directory a git repo: ${isGitRepository(workdir)}
|
|
128
|
+
Platform: ${os.platform()}
|
|
129
|
+
OS Version: ${os.type()} ${os.release()}
|
|
130
|
+
Today's date: ${new Date().toISOString().split("T")[0]}
|
|
131
|
+
</env>
|
|
103
132
|
`;
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
134
|
+
// If there is memory content, add it to the system prompt
|
|
135
|
+
if (memory && memory.trim()) {
|
|
136
|
+
systemContent += `\n## Memory Context\n\nThe following is important context and memory from previous interactions:\n\n${memory}`;
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
// Add system prompt
|
package/src/services/memory.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const addMemory = async (
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const memoryFilePath = path.join(workdir, "
|
|
19
|
+
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
20
20
|
|
|
21
21
|
// Format memory entry, starting with -, no timestamp
|
|
22
22
|
const memoryEntry = `- ${message.substring(1).trim()}\n`;
|
|
@@ -113,7 +113,7 @@ export const getUserMemoryContent = async (): Promise<string> => {
|
|
|
113
113
|
// Read project memory file content
|
|
114
114
|
export const readMemoryFile = async (workdir: string): Promise<string> => {
|
|
115
115
|
try {
|
|
116
|
-
const memoryFilePath = path.join(workdir, "
|
|
116
|
+
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
117
117
|
return await fs.readFile(memoryFilePath, "utf-8");
|
|
118
118
|
} catch (error) {
|
|
119
119
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
package/src/tools/grepTool.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const grepTool: ToolPlugin = {
|
|
|
71
71
|
head_limit: {
|
|
72
72
|
type: "number",
|
|
73
73
|
description:
|
|
74
|
-
'Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries).
|
|
74
|
+
'Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults to 100 to prevent excessive token usage.',
|
|
75
75
|
},
|
|
76
76
|
multiline: {
|
|
77
77
|
type: "boolean",
|
|
@@ -209,12 +209,15 @@ export const grepTool: ToolPlugin = {
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
// Apply head_limit
|
|
212
|
+
// Apply head_limit with default fallback
|
|
213
213
|
let finalOutput = output;
|
|
214
214
|
let lines = output.split("\n");
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
// Set default head_limit if not specified to prevent excessive token usage
|
|
217
|
+
const effectiveHeadLimit = headLimit || 100;
|
|
218
|
+
|
|
219
|
+
if (lines.length > effectiveHeadLimit) {
|
|
220
|
+
lines = lines.slice(0, effectiveHeadLimit);
|
|
218
221
|
finalOutput = lines.join("\n");
|
|
219
222
|
}
|
|
220
223
|
|
|
@@ -230,8 +233,8 @@ export const grepTool: ToolPlugin = {
|
|
|
230
233
|
shortResult = `Found ${totalLines} matching line${totalLines === 1 ? "" : "s"}`;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
if (
|
|
234
|
-
shortResult += ` (showing first ${
|
|
236
|
+
if (effectiveHeadLimit && totalLines > effectiveHeadLimit) {
|
|
237
|
+
shortResult += ` (showing first ${effectiveHeadLimit})`;
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
return {
|
package/src/tools/readTool.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
3
3
|
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
4
|
+
import {
|
|
5
|
+
isBinaryDocument,
|
|
6
|
+
getBinaryDocumentError,
|
|
7
|
+
} from "../utils/fileFormat.js";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Read Tool Plugin - Read file content
|
|
@@ -12,7 +16,7 @@ export const readTool: ToolPlugin = {
|
|
|
12
16
|
function: {
|
|
13
17
|
name: "Read",
|
|
14
18
|
description:
|
|
15
|
-
"Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read
|
|
19
|
+
"Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.\n- Binary document formats (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX) are not supported and will return an error.",
|
|
16
20
|
parameters: {
|
|
17
21
|
type: "object",
|
|
18
22
|
properties: {
|
|
@@ -51,6 +55,15 @@ export const readTool: ToolPlugin = {
|
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
// Check for binary document formats
|
|
59
|
+
if (isBinaryDocument(filePath)) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
content: "",
|
|
63
|
+
error: getBinaryDocumentError(filePath),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
try {
|
|
55
68
|
// Note: New Read tool requires absolute paths, so we don't use resolvePath
|
|
56
69
|
// But for compatibility, if it's not an absolute path, we still try to resolve
|
|
@@ -71,8 +84,19 @@ export const readTool: ToolPlugin = {
|
|
|
71
84
|
};
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
// Check content size limit (100KB)
|
|
88
|
+
const MAX_CONTENT_SIZE = 100 * 1024; // 100KB
|
|
89
|
+
let contentToProcess = fileContent;
|
|
90
|
+
let contentTruncated = false;
|
|
91
|
+
|
|
92
|
+
if (fileContent.length > MAX_CONTENT_SIZE) {
|
|
93
|
+
contentToProcess = fileContent.substring(0, MAX_CONTENT_SIZE);
|
|
94
|
+
contentTruncated = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lines = contentToProcess.split("\n");
|
|
75
98
|
const totalLines = lines.length;
|
|
99
|
+
const originalTotalLines = fileContent.split("\n").length;
|
|
76
100
|
|
|
77
101
|
// Handle offset and limit
|
|
78
102
|
let startLine = 1;
|
|
@@ -117,7 +141,10 @@ export const readTool: ToolPlugin = {
|
|
|
117
141
|
|
|
118
142
|
// Add file information header
|
|
119
143
|
let content = `File: ${filePath}\n`;
|
|
120
|
-
if (
|
|
144
|
+
if (contentTruncated) {
|
|
145
|
+
content += `Content truncated at ${MAX_CONTENT_SIZE} bytes\n`;
|
|
146
|
+
content += `Lines ${startLine}-${endLine} of ${totalLines} (original file: ${originalTotalLines} lines)\n`;
|
|
147
|
+
} else if (startLine > 1 || endLine < totalLines) {
|
|
121
148
|
content += `Lines ${startLine}-${endLine} of ${totalLines}\n`;
|
|
122
149
|
} else {
|
|
123
150
|
content += `Total lines: ${totalLines}\n`;
|
|
@@ -126,15 +153,22 @@ export const readTool: ToolPlugin = {
|
|
|
126
153
|
content += formattedContent;
|
|
127
154
|
|
|
128
155
|
// If only showing partial content, add prompt
|
|
129
|
-
if (endLine < totalLines) {
|
|
156
|
+
if (endLine < totalLines || contentTruncated) {
|
|
130
157
|
content += `\n${"─".repeat(50)}\n`;
|
|
131
|
-
|
|
158
|
+
if (contentTruncated) {
|
|
159
|
+
content += `... content truncated due to size limit (${MAX_CONTENT_SIZE} bytes)`;
|
|
160
|
+
if (endLine < totalLines) {
|
|
161
|
+
content += ` and ${totalLines - endLine} more lines not shown`;
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
content += `... ${totalLines - endLine} more lines not shown`;
|
|
165
|
+
}
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
return {
|
|
135
169
|
success: true,
|
|
136
170
|
content,
|
|
137
|
-
shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 ? " (truncated)" : ""}`,
|
|
171
|
+
shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 || contentTruncated ? " (truncated)" : ""}`,
|
|
138
172
|
};
|
|
139
173
|
} catch (error) {
|
|
140
174
|
return {
|