wave-agent-sdk 0.0.1 → 0.0.3

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.
Files changed (82) hide show
  1. package/dist/agent.d.ts +37 -3
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +82 -5
  4. package/dist/index.d.ts +0 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +0 -1
  7. package/dist/managers/aiManager.d.ts +7 -1
  8. package/dist/managers/aiManager.d.ts.map +1 -1
  9. package/dist/managers/aiManager.js +11 -5
  10. package/dist/managers/messageManager.d.ts +8 -0
  11. package/dist/managers/messageManager.d.ts.map +1 -1
  12. package/dist/managers/messageManager.js +26 -2
  13. package/dist/managers/skillManager.d.ts +4 -5
  14. package/dist/managers/skillManager.d.ts.map +1 -1
  15. package/dist/managers/skillManager.js +6 -82
  16. package/dist/managers/subagentManager.d.ts +96 -0
  17. package/dist/managers/subagentManager.d.ts.map +1 -0
  18. package/dist/managers/subagentManager.js +261 -0
  19. package/dist/managers/toolManager.d.ts +33 -1
  20. package/dist/managers/toolManager.d.ts.map +1 -1
  21. package/dist/managers/toolManager.js +43 -5
  22. package/dist/services/aiService.d.ts +5 -0
  23. package/dist/services/aiService.d.ts.map +1 -1
  24. package/dist/services/aiService.js +58 -28
  25. package/dist/services/session.d.ts.map +1 -1
  26. package/dist/services/session.js +4 -0
  27. package/dist/tools/grepTool.d.ts.map +1 -1
  28. package/dist/tools/grepTool.js +8 -6
  29. package/dist/tools/readTool.d.ts.map +1 -1
  30. package/dist/tools/readTool.js +36 -6
  31. package/dist/tools/skillTool.d.ts +8 -0
  32. package/dist/tools/skillTool.d.ts.map +1 -0
  33. package/dist/tools/skillTool.js +72 -0
  34. package/dist/tools/taskTool.d.ts +8 -0
  35. package/dist/tools/taskTool.d.ts.map +1 -0
  36. package/dist/tools/taskTool.js +109 -0
  37. package/dist/tools/todoWriteTool.d.ts +6 -0
  38. package/dist/tools/todoWriteTool.d.ts.map +1 -0
  39. package/dist/tools/todoWriteTool.js +203 -0
  40. package/dist/types.d.ts +65 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +16 -0
  43. package/dist/utils/configResolver.d.ts +38 -0
  44. package/dist/utils/configResolver.d.ts.map +1 -0
  45. package/dist/utils/configResolver.js +106 -0
  46. package/dist/utils/configValidator.d.ts +36 -0
  47. package/dist/utils/configValidator.d.ts.map +1 -0
  48. package/dist/utils/configValidator.js +78 -0
  49. package/dist/utils/constants.d.ts +10 -0
  50. package/dist/utils/constants.d.ts.map +1 -1
  51. package/dist/utils/constants.js +10 -0
  52. package/dist/utils/fileFormat.d.ts +17 -0
  53. package/dist/utils/fileFormat.d.ts.map +1 -0
  54. package/dist/utils/fileFormat.js +35 -0
  55. package/dist/utils/messageOperations.d.ts +18 -0
  56. package/dist/utils/messageOperations.d.ts.map +1 -1
  57. package/dist/utils/messageOperations.js +43 -0
  58. package/dist/utils/subagentParser.d.ts +19 -0
  59. package/dist/utils/subagentParser.d.ts.map +1 -0
  60. package/dist/utils/subagentParser.js +159 -0
  61. package/package.json +11 -15
  62. package/src/agent.ts +130 -9
  63. package/src/index.ts +0 -1
  64. package/src/managers/aiManager.ts +22 -10
  65. package/src/managers/messageManager.ts +55 -1
  66. package/src/managers/skillManager.ts +7 -96
  67. package/src/managers/subagentManager.ts +368 -0
  68. package/src/managers/toolManager.ts +50 -5
  69. package/src/services/aiService.ts +92 -36
  70. package/src/services/session.ts +5 -0
  71. package/src/tools/grepTool.ts +9 -6
  72. package/src/tools/readTool.ts +40 -6
  73. package/src/tools/skillTool.ts +82 -0
  74. package/src/tools/taskTool.ts +128 -0
  75. package/src/tools/todoWriteTool.ts +232 -0
  76. package/src/types.ts +85 -1
  77. package/src/utils/configResolver.ts +142 -0
  78. package/src/utils/configValidator.ts +133 -0
  79. package/src/utils/constants.ts +10 -0
  80. package/src/utils/fileFormat.ts +40 -0
  81. package/src/utils/messageOperations.ts +80 -0
  82. package/src/utils/subagentParser.ts +223 -0
@@ -5,12 +5,40 @@ import {
5
5
  ChatCompletionMessageParam,
6
6
  ChatCompletionFunctionTool,
7
7
  } from "openai/resources.js";
8
- import { FAST_MODEL_ID, AGENT_MODEL_ID } from "@/utils/constants.js";
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";
9
12
 
10
13
  /**
11
- * Model configuration type, based on OpenAI parameters but excluding messages
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
12
17
  */
13
- type ModelConfig = Omit<ChatCompletionCreateParamsNonStreaming, "messages">;
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
+ }
34
+
35
+ /**
36
+ * OpenAI model configuration type, based on OpenAI parameters but excluding messages
37
+ */
38
+ type OpenAIModelConfig = Omit<
39
+ ChatCompletionCreateParamsNonStreaming,
40
+ "messages"
41
+ >;
14
42
 
15
43
  /**
16
44
  * Get specific configuration parameters based on model name
@@ -20,9 +48,9 @@ type ModelConfig = Omit<ChatCompletionCreateParamsNonStreaming, "messages">;
20
48
  */
21
49
  function getModelConfig(
22
50
  modelName: string,
23
- baseConfig: Partial<ModelConfig> = {},
24
- ): ModelConfig {
25
- const config: ModelConfig = {
51
+ baseConfig: Partial<OpenAIModelConfig> = {},
52
+ ): OpenAIModelConfig {
53
+ const config: OpenAIModelConfig = {
26
54
  model: modelName,
27
55
  stream: false,
28
56
  ...baseConfig,
@@ -37,13 +65,12 @@ function getModelConfig(
37
65
  return config;
38
66
  }
39
67
 
40
- // Initialize OpenAI client with environment variables
41
- const openai = new OpenAI({
42
- apiKey: process.env.AIGW_TOKEN,
43
- baseURL: process.env.AIGW_URL,
44
- });
45
-
46
68
  export interface CallAgentOptions {
69
+ // Resolved configuration
70
+ gatewayConfig: GatewayConfig;
71
+ modelConfig: ModelConfig;
72
+
73
+ // Existing parameters (preserved)
47
74
  messages: ChatCompletionMessageParam[];
48
75
  sessionId?: string;
49
76
  abortSignal?: AbortSignal;
@@ -67,28 +94,46 @@ export interface CallAgentResult {
67
94
  export async function callAgent(
68
95
  options: CallAgentOptions,
69
96
  ): Promise<CallAgentResult> {
70
- const { messages, abortSignal, memory, workdir, tools, model, systemPrompt } =
71
- options;
97
+ const {
98
+ gatewayConfig,
99
+ modelConfig,
100
+ messages,
101
+ abortSignal,
102
+ memory,
103
+ workdir,
104
+ tools,
105
+ model,
106
+ systemPrompt,
107
+ } = options;
72
108
 
73
109
  try {
74
- // Build system prompt content
75
- let systemContent: string;
76
-
77
- if (systemPrompt) {
78
- // Use custom system prompt if provided
79
- systemContent = systemPrompt;
80
- } else {
81
- // Use default system prompt
82
- systemContent = `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.
110
+ // Create OpenAI client with injected configuration
111
+ const openai = new OpenAI({
112
+ apiKey: gatewayConfig.apiKey,
113
+ baseURL: gatewayConfig.baseURL,
114
+ });
83
115
 
84
- ## Current Working Directory
85
- ${workdir}
116
+ // Build system prompt content
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>
86
132
  `;
87
133
 
88
- // If there is memory content, add it to the system prompt
89
- if (memory && memory.trim()) {
90
- systemContent += `\n\n## Memory Context\n\nThe following is important context and memory from previous interactions:\n\n${memory}`;
91
- }
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}`;
92
137
  }
93
138
 
94
139
  // Add system prompt
@@ -101,15 +146,15 @@ ${workdir}
101
146
  const openaiMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] =
102
147
  [systemMessage, ...messages];
103
148
 
104
- // Get model configuration
105
- const modelConfig = getModelConfig(model || AGENT_MODEL_ID, {
149
+ // Get model configuration - use injected modelConfig with optional override
150
+ const openaiModelConfig = getModelConfig(model || modelConfig.agentModel, {
106
151
  temperature: 0,
107
152
  max_completion_tokens: 32768,
108
153
  });
109
154
 
110
155
  // Prepare API call parameters
111
156
  const createParams: ChatCompletionCreateParamsNonStreaming = {
112
- ...modelConfig,
157
+ ...openaiModelConfig,
113
158
  messages: openaiMessages,
114
159
  };
115
160
 
@@ -160,6 +205,11 @@ ${workdir}
160
205
  }
161
206
 
162
207
  export interface CompressMessagesOptions {
208
+ // Resolved configuration
209
+ gatewayConfig: GatewayConfig;
210
+ modelConfig: ModelConfig;
211
+
212
+ // Existing parameters
163
213
  messages: ChatCompletionMessageParam[];
164
214
  abortSignal?: AbortSignal;
165
215
  }
@@ -167,10 +217,16 @@ export interface CompressMessagesOptions {
167
217
  export async function compressMessages(
168
218
  options: CompressMessagesOptions,
169
219
  ): Promise<string> {
170
- const { messages, abortSignal } = options;
220
+ const { gatewayConfig, modelConfig, messages, abortSignal } = options;
221
+
222
+ // Create OpenAI client with injected configuration
223
+ const openai = new OpenAI({
224
+ apiKey: gatewayConfig.apiKey,
225
+ baseURL: gatewayConfig.baseURL,
226
+ });
171
227
 
172
- // Get model configuration
173
- const modelConfig = getModelConfig(FAST_MODEL_ID, {
228
+ // Get model configuration - use injected fast model
229
+ const openaiModelConfig = getModelConfig(modelConfig.fastModel, {
174
230
  temperature: 0.1,
175
231
  max_tokens: 1500,
176
232
  });
@@ -178,7 +234,7 @@ export async function compressMessages(
178
234
  try {
179
235
  const response = await openai.chat.completions.create(
180
236
  {
181
- ...modelConfig,
237
+ ...openaiModelConfig,
182
238
  messages: [
183
239
  {
184
240
  role: "system",
@@ -76,6 +76,11 @@ export async function saveSession(
76
76
  return;
77
77
  }
78
78
 
79
+ // Do not save if there are no messages
80
+ if (messages.length === 0) {
81
+ return;
82
+ }
83
+
79
84
  await ensureSessionDir();
80
85
 
81
86
  // Filter out diff blocks before saving
@@ -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). When unspecified, shows all results from ripgrep.',
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
- if (headLimit && headLimit > 0 && lines.length > headLimit) {
217
- lines = lines.slice(0, headLimit);
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 (headLimit && totalLines > headLimit) {
234
- shortResult += ` (showing first ${headLimit})`;
236
+ if (effectiveHeadLimit && totalLines > effectiveHeadLimit) {
237
+ shortResult += ` (showing first ${effectiveHeadLimit})`;
235
238
  }
236
239
 
237
240
  return {
@@ -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 PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\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.",
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
- const lines = fileContent.split("\n");
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 (startLine > 1 || endLine < totalLines) {
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
- content += `... ${totalLines - endLine} more lines not shown`;
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 {
@@ -0,0 +1,82 @@
1
+ import type { ToolPlugin, ToolResult } from "./types.js";
2
+ import type { SkillManager } from "../managers/skillManager.js";
3
+
4
+ /**
5
+ * Create a skill tool plugin that uses the provided SkillManager
6
+ * Note: SkillManager should be initialized before calling this function
7
+ */
8
+ export function createSkillTool(skillManager: SkillManager): ToolPlugin {
9
+ const getToolDescription = (): string => {
10
+ const availableSkills = skillManager.getAvailableSkills();
11
+
12
+ if (availableSkills.length === 0) {
13
+ return "Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific. No skills are currently available.";
14
+ }
15
+
16
+ const skillList = availableSkills
17
+ .map(
18
+ (skill) => `• **${skill.name}** (${skill.type}): ${skill.description}`,
19
+ )
20
+ .join("\n");
21
+
22
+ return `Invoke a Wave skill by name. Skills are user-defined automation templates that can be personal or project-specific.\n\nAvailable skills:\n${skillList}`;
23
+ };
24
+
25
+ return {
26
+ name: "skill",
27
+ config: {
28
+ type: "function",
29
+ function: {
30
+ name: "skill",
31
+ description: getToolDescription(),
32
+ parameters: {
33
+ type: "object",
34
+ properties: {
35
+ skill_name: {
36
+ type: "string",
37
+ description: "Name of the skill to invoke",
38
+ enum: skillManager
39
+ .getAvailableSkills()
40
+ .map((skill) => skill.name),
41
+ },
42
+ },
43
+ required: ["skill_name"],
44
+ },
45
+ },
46
+ },
47
+ execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
48
+ try {
49
+ // Validate arguments
50
+ const skillName = args.skill_name as string;
51
+ if (!skillName || typeof skillName !== "string") {
52
+ return {
53
+ success: false,
54
+ content: "",
55
+ error: "skill_name parameter is required and must be a string",
56
+ };
57
+ }
58
+
59
+ // Execute the skill
60
+ const result = await skillManager.executeSkill({
61
+ skill_name: skillName,
62
+ });
63
+
64
+ return {
65
+ success: true,
66
+ content: result.content,
67
+ shortResult: `Invoked skill: ${skillName}`,
68
+ };
69
+ } catch (error) {
70
+ return {
71
+ success: false,
72
+ content: "",
73
+ error: error instanceof Error ? error.message : String(error),
74
+ };
75
+ }
76
+ },
77
+ formatCompactParams: (params: Record<string, unknown>) => {
78
+ const skillName = params.skill_name as string;
79
+ return skillName || "unknown-skill";
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,128 @@
1
+ import type { ToolPlugin, ToolResult } from "./types.js";
2
+ import type { SubagentManager } from "../managers/subagentManager.js";
3
+
4
+ /**
5
+ * Create a task tool plugin that uses the provided SubagentManager
6
+ * Note: SubagentManager should be initialized before calling this function
7
+ */
8
+ export function createTaskTool(subagentManager: SubagentManager): ToolPlugin {
9
+ // Get available subagents from the initialized subagent manager
10
+ const availableSubagents = subagentManager.getConfigurations();
11
+ const subagentList = availableSubagents
12
+ .map((config) => `- ${config.name}: ${config.description}`)
13
+ .join("\n");
14
+
15
+ const description = `Delegate a task to a specialized subagent. Use this when you need specialized expertise or want to break down complex work into focused subtasks.
16
+
17
+ Available subagents:
18
+ ${subagentList || "No subagents configured"}`;
19
+
20
+ return {
21
+ name: "Task",
22
+ config: {
23
+ type: "function",
24
+ function: {
25
+ name: "Task",
26
+ description,
27
+ parameters: {
28
+ type: "object",
29
+ properties: {
30
+ description: {
31
+ type: "string",
32
+ description:
33
+ "A clear, concise description of what needs to be accomplished",
34
+ },
35
+ prompt: {
36
+ type: "string",
37
+ description:
38
+ "The specific instructions or prompt to send to the subagent",
39
+ },
40
+ subagent_type: {
41
+ type: "string",
42
+ description: `The type or name of subagent to use. Available options: ${availableSubagents.map((c) => c.name).join(", ") || "none"}`,
43
+ },
44
+ },
45
+ required: ["description", "prompt", "subagent_type"],
46
+ },
47
+ },
48
+ },
49
+
50
+ execute: async (args: Record<string, unknown>): Promise<ToolResult> => {
51
+ // Input validation
52
+ const description = args.description as string;
53
+ const prompt = args.prompt as string;
54
+ const subagent_type = args.subagent_type as string;
55
+
56
+ if (!description || typeof description !== "string") {
57
+ return {
58
+ success: false,
59
+ content: "",
60
+ error: "description parameter is required and must be a string",
61
+ shortResult: "Task delegation failed",
62
+ };
63
+ }
64
+
65
+ if (!prompt || typeof prompt !== "string") {
66
+ return {
67
+ success: false,
68
+ content: "",
69
+ error: "prompt parameter is required and must be a string",
70
+ shortResult: "Task delegation failed",
71
+ };
72
+ }
73
+
74
+ if (!subagent_type || typeof subagent_type !== "string") {
75
+ return {
76
+ success: false,
77
+ content: "",
78
+ error: "subagent_type parameter is required and must be a string",
79
+ shortResult: "Task delegation failed",
80
+ };
81
+ }
82
+
83
+ try {
84
+ // Subagent selection logic with explicit name matching only
85
+ const configuration = await subagentManager.findSubagent(subagent_type);
86
+
87
+ if (!configuration) {
88
+ // Error handling for nonexistent subagents with available subagents listing
89
+ const allConfigs = subagentManager.getConfigurations();
90
+ const availableNames = allConfigs.map((c) => c.name).join(", ");
91
+
92
+ return {
93
+ success: false,
94
+ content: "",
95
+ error: `No subagent found matching "${subagent_type}". Available subagents: ${availableNames || "none"}`,
96
+ shortResult: "Subagent not found",
97
+ };
98
+ }
99
+
100
+ // Create subagent instance and execute task
101
+ const instance = await subagentManager.createInstance(
102
+ configuration,
103
+ description,
104
+ );
105
+ const response = await subagentManager.executeTask(instance, prompt);
106
+
107
+ return {
108
+ success: true,
109
+ content: response,
110
+ shortResult: `Task completed by ${configuration.name}`,
111
+ };
112
+ } catch (error) {
113
+ return {
114
+ success: false,
115
+ content: "",
116
+ error: `Task delegation failed: ${error instanceof Error ? error.message : String(error)}`,
117
+ shortResult: "Delegation error",
118
+ };
119
+ }
120
+ },
121
+
122
+ formatCompactParams: (params: Record<string, unknown>) => {
123
+ const subagent_type = params.subagent_type as string;
124
+ const description = params.description as string;
125
+ return `${subagent_type || "unknown"}: ${description || "no description"}`;
126
+ },
127
+ };
128
+ }