@banaxi/banana-code 1.2.0 → 1.4.0
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/package.json +1 -1
- package/src/config.js +23 -1
- package/src/constants.js +9 -0
- package/src/index.js +10 -5
- package/src/prompt.js +13 -0
- package/src/providers/claude.js +1 -1
- package/src/providers/gemini.js +1 -1
- package/src/providers/mistral.js +141 -0
- package/src/providers/ollamaCloud.js +44 -15
- package/src/providers/openai.js +2 -2
- package/src/tools/delegateTask.js +100 -0
- package/src/tools/registry.js +26 -1
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { execSync } from 'child_process';
|
|
|
6
6
|
import fsSync from 'fs';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
|
|
9
|
-
import { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS } from './constants.js';
|
|
9
|
+
import { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS, MISTRAL_MODELS } from './constants.js';
|
|
10
10
|
|
|
11
11
|
const CONFIG_DIR = path.join(os.homedir(), '.config', 'banana-code');
|
|
12
12
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -74,6 +74,27 @@ export async function setupProvider(provider, config = {}) {
|
|
|
74
74
|
message: 'Select a Claude model:',
|
|
75
75
|
choices: CLAUDE_MODELS
|
|
76
76
|
});
|
|
77
|
+
} else if (provider === 'mistral') {
|
|
78
|
+
config.apiKey = await input({
|
|
79
|
+
message: 'Enter your MISTRAL_API_KEY (from console.mistral.ai):',
|
|
80
|
+
default: config.apiKey
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const choices = [...MISTRAL_MODELS, { name: chalk.magenta('✎ Enter custom model ID...'), value: 'CUSTOM_ID' }];
|
|
84
|
+
let selectedModel = await select({
|
|
85
|
+
message: 'Select a Mistral model:',
|
|
86
|
+
choices,
|
|
87
|
+
loop: false,
|
|
88
|
+
pageSize: Math.max(choices.length, 15)
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (selectedModel === 'CUSTOM_ID') {
|
|
92
|
+
selectedModel = await input({
|
|
93
|
+
message: 'Enter the exact Mistral model ID (e.g., mistral-large-latest):',
|
|
94
|
+
validate: (v) => v.trim().length > 0 || 'Model ID cannot be empty'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
config.model = selectedModel;
|
|
77
98
|
} else if (provider === 'openai') {
|
|
78
99
|
const authMethod = await select({
|
|
79
100
|
message: 'How would you like to authenticate with OpenAI?',
|
|
@@ -158,6 +179,7 @@ async function runSetupWizard() {
|
|
|
158
179
|
{ name: 'Google Gemini', value: 'gemini' },
|
|
159
180
|
{ name: 'Anthropic Claude', value: 'claude' },
|
|
160
181
|
{ name: 'OpenAI', value: 'openai' },
|
|
182
|
+
{ name: 'Mistral AI', value: 'mistral' },
|
|
161
183
|
{ name: 'Ollama Cloud', value: 'ollama_cloud' },
|
|
162
184
|
{ name: 'Ollama (Local)', value: 'ollama' }
|
|
163
185
|
]
|
package/src/constants.js
CHANGED
|
@@ -30,6 +30,15 @@ export const OLLAMA_CLOUD_MODELS = [
|
|
|
30
30
|
{ name: 'Llama 3.1 405B (Cloud)', value: 'llama3.1:405b-cloud' }
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
+
export const MISTRAL_MODELS = [
|
|
34
|
+
{ name: 'Mistral Large (Latest)', value: 'mistral-large-latest' },
|
|
35
|
+
{ name: 'Mistral Medium (Latest)', value: 'mistral-medium-latest' },
|
|
36
|
+
{ name: 'Mistral Small (Latest)', value: 'mistral-small-latest' },
|
|
37
|
+
{ name: 'Codestral (Latest)', value: 'codestral-latest' },
|
|
38
|
+
{ name: 'Mistral Nemo', value: 'open-mistral-nemo' },
|
|
39
|
+
{ name: 'Pixtral 12B', value: 'pixtral-12b-2409' }
|
|
40
|
+
];
|
|
41
|
+
|
|
33
42
|
export const CODEX_MODELS = [
|
|
34
43
|
{ name: 'GPT-5.4 (Newest)', value: 'gpt-5.4' },
|
|
35
44
|
{ name: 'GPT-5.3 Codex', value: 'gpt-5.3-codex' },
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ClaudeProvider } from './providers/claude.js';
|
|
|
10
10
|
import { OpenAIProvider } from './providers/openai.js';
|
|
11
11
|
import { OllamaProvider } from './providers/ollama.js';
|
|
12
12
|
import { OllamaCloudProvider } from './providers/ollamaCloud.js';
|
|
13
|
+
import { MistralProvider } from './providers/mistral.js';
|
|
13
14
|
|
|
14
15
|
import { loadSession, saveSession, generateSessionId, getLatestSessionId, listSessions } from './sessions.js';
|
|
15
16
|
import { printMarkdown } from './utils/markdown.js';
|
|
@@ -28,6 +29,7 @@ function createProvider(overrideConfig = null) {
|
|
|
28
29
|
case 'gemini': return new GeminiProvider(activeConfig);
|
|
29
30
|
case 'claude': return new ClaudeProvider(activeConfig);
|
|
30
31
|
case 'openai': return new OpenAIProvider(activeConfig);
|
|
32
|
+
case 'mistral': return new MistralProvider(activeConfig);
|
|
31
33
|
case 'ollama_cloud': return new OllamaCloudProvider(activeConfig);
|
|
32
34
|
case 'ollama': return new OllamaProvider(activeConfig);
|
|
33
35
|
default:
|
|
@@ -51,20 +53,21 @@ async function handleSlashCommand(command) {
|
|
|
51
53
|
{ name: 'Google Gemini', value: 'gemini' },
|
|
52
54
|
{ name: 'Anthropic Claude', value: 'claude' },
|
|
53
55
|
{ name: 'OpenAI', value: 'openai' },
|
|
56
|
+
{ name: 'Mistral AI', value: 'mistral' },
|
|
54
57
|
{ name: 'Ollama Cloud', value: 'ollama_cloud' },
|
|
55
58
|
{ name: 'Ollama (Local)', value: 'ollama' }
|
|
56
59
|
]
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
if (['gemini', 'claude', 'openai', 'ollama_cloud', 'ollama'].includes(newProv)) {
|
|
63
|
+
if (['gemini', 'claude', 'openai', 'mistral', 'ollama_cloud', 'ollama'].includes(newProv)) {
|
|
61
64
|
// Use the shared setup logic to get keys/models
|
|
62
65
|
config = await setupProvider(newProv, config);
|
|
63
66
|
await saveConfig(config);
|
|
64
67
|
providerInstance = createProvider();
|
|
65
68
|
console.log(chalk.green(`Switched provider to ${newProv} (${config.model}).`));
|
|
66
69
|
} else {
|
|
67
|
-
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|ollama_cloud|ollama>`));
|
|
70
|
+
console.log(chalk.yellow(`Usage: /provider <gemini|claude|openai|mistral|ollama_cloud|ollama>`));
|
|
68
71
|
}
|
|
69
72
|
break;
|
|
70
73
|
case '/model':
|
|
@@ -72,13 +75,15 @@ async function handleSlashCommand(command) {
|
|
|
72
75
|
if (!newModel) {
|
|
73
76
|
// Interactive selection
|
|
74
77
|
const { select } = await import('@inquirer/prompts');
|
|
75
|
-
const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS } = await import('./constants.js');
|
|
78
|
+
const { GEMINI_MODELS, CLAUDE_MODELS, OPENAI_MODELS, CODEX_MODELS, OLLAMA_CLOUD_MODELS, MISTRAL_MODELS } = await import('./constants.js');
|
|
76
79
|
|
|
77
80
|
let choices = [];
|
|
78
81
|
if (config.provider === 'gemini') choices = GEMINI_MODELS;
|
|
79
82
|
else if (config.provider === 'claude') choices = CLAUDE_MODELS;
|
|
80
83
|
else if (config.provider === 'openai') {
|
|
81
84
|
choices = config.authType === 'oauth' ? CODEX_MODELS : OPENAI_MODELS;
|
|
85
|
+
} else if (config.provider === 'mistral') {
|
|
86
|
+
choices = MISTRAL_MODELS;
|
|
82
87
|
} else if (config.provider === 'ollama_cloud') {
|
|
83
88
|
choices = OLLAMA_CLOUD_MODELS;
|
|
84
89
|
} else if (config.provider === 'ollama') {
|
|
@@ -94,7 +99,7 @@ async function handleSlashCommand(command) {
|
|
|
94
99
|
|
|
95
100
|
if (choices.length > 0) {
|
|
96
101
|
const finalChoices = [...choices];
|
|
97
|
-
if (config.provider === 'ollama_cloud') {
|
|
102
|
+
if (config.provider === 'ollama_cloud' || config.provider === 'mistral') {
|
|
98
103
|
finalChoices.push({ name: chalk.magenta('✎ Enter custom model ID...'), value: 'CUSTOM_ID' });
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -381,7 +386,7 @@ async function handleSlashCommand(command) {
|
|
|
381
386
|
case '/help':
|
|
382
387
|
console.log(chalk.yellow(`
|
|
383
388
|
Available commands:
|
|
384
|
-
/provider <name> - Switch AI provider (gemini, claude, openai, ollama_cloud, ollama)
|
|
389
|
+
/provider <name> - Switch AI provider (gemini, claude, openai, mistral, ollama_cloud, ollama)
|
|
385
390
|
/model [name] - Switch model within current provider (opens menu if name omitted)
|
|
386
391
|
/chats - List persistent chat sessions
|
|
387
392
|
/clear - Clear chat history
|
package/src/prompt.js
CHANGED
|
@@ -37,6 +37,19 @@ Always use tools when they would help. Be concise but thorough. `;
|
|
|
37
37
|
prompt += `</available_skills>\n\nOnce a skill is activated, its instructions and resources are returned wrapped in <activated_skill> tags. You MUST treat the content within <instructions> as expert procedural guidance for the duration of the task.\n`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const hasDelegateTool = availableToolsList.some(t => t.name === 'delegate_task');
|
|
41
|
+
if (hasDelegateTool) {
|
|
42
|
+
prompt += `
|
|
43
|
+
\n# Sub-Agent Delegation
|
|
44
|
+
You have the ability to spawn specialized sub-agents to handle complex sub-tasks using the \`delegate_task\` tool.
|
|
45
|
+
- Use **researcher** for deep codebase exploration or fact-finding.
|
|
46
|
+
- Use **coder** for implementing specific features or complex bug fixes.
|
|
47
|
+
- Use **reviewer** for analyzing code quality or security.
|
|
48
|
+
- Use **generalist** for any other multi-step sub-task.
|
|
49
|
+
Delegation is highly recommended for tasks that would otherwise bloat your current conversation context. The results of the sub-agent will be returned to you as a summary.
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
if (config.planMode) {
|
|
41
54
|
prompt += `
|
|
42
55
|
[PLAN MODE ENABLED]
|
package/src/providers/claude.js
CHANGED
|
@@ -114,7 +114,7 @@ export class ClaudeProvider {
|
|
|
114
114
|
const toolResultContent = [];
|
|
115
115
|
for (const call of toolCalls) {
|
|
116
116
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
|
|
117
|
-
const res = await executeTool(call.name, call.input);
|
|
117
|
+
const res = await executeTool(call.name, call.input, this.config);
|
|
118
118
|
if (this.config.debug) {
|
|
119
119
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
120
120
|
}
|
package/src/providers/gemini.js
CHANGED
|
@@ -135,7 +135,7 @@ export class GeminiProvider {
|
|
|
135
135
|
hasToolCalls = true;
|
|
136
136
|
const call = part.functionCall;
|
|
137
137
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.name}]`));
|
|
138
|
-
const res = await executeTool(call.name, call.args);
|
|
138
|
+
const res = await executeTool(call.name, call.args, this.config);
|
|
139
139
|
if (this.config.debug) {
|
|
140
140
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
141
141
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { getAvailableTools, executeTool } from '../tools/registry.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { getSystemPrompt } from '../prompt.js';
|
|
6
|
+
import { printMarkdown } from '../utils/markdown.js';
|
|
7
|
+
|
|
8
|
+
export class MistralProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.openai = new OpenAI({
|
|
12
|
+
apiKey: config.apiKey,
|
|
13
|
+
baseURL: 'https://api.mistral.ai/v1'
|
|
14
|
+
});
|
|
15
|
+
this.modelName = config.model || 'mistral-large-latest';
|
|
16
|
+
this.systemPrompt = getSystemPrompt(config);
|
|
17
|
+
this.messages = [{ role: 'system', content: this.systemPrompt }];
|
|
18
|
+
this.tools = getAvailableTools(config).map(t => ({
|
|
19
|
+
type: 'function',
|
|
20
|
+
function: {
|
|
21
|
+
name: t.name,
|
|
22
|
+
description: t.description,
|
|
23
|
+
parameters: t.parameters
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateSystemPrompt(newPrompt) {
|
|
29
|
+
this.systemPrompt = newPrompt;
|
|
30
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system') {
|
|
31
|
+
this.messages[0].content = newPrompt;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async sendMessage(message) {
|
|
36
|
+
this.messages.push({ role: 'user', content: message });
|
|
37
|
+
|
|
38
|
+
let spinner = ora({ text: 'Thinking...', color: 'yellow', stream: process.stdout }).start();
|
|
39
|
+
let finalResponse = '';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
while (true) {
|
|
43
|
+
let stream = null;
|
|
44
|
+
try {
|
|
45
|
+
stream = await this.openai.chat.completions.create({
|
|
46
|
+
model: this.modelName,
|
|
47
|
+
messages: this.messages,
|
|
48
|
+
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
49
|
+
stream: true
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
spinner.stop();
|
|
53
|
+
console.error(chalk.red(`Mistral Request Error: ${e.message}`));
|
|
54
|
+
return `Error: ${e.message}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let chunkResponse = '';
|
|
58
|
+
let toolCalls = [];
|
|
59
|
+
|
|
60
|
+
for await (const chunk of stream) {
|
|
61
|
+
const delta = chunk.choices[0]?.delta;
|
|
62
|
+
|
|
63
|
+
if (delta?.content) {
|
|
64
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
65
|
+
if (!this.config.useMarkedTerminal) {
|
|
66
|
+
process.stdout.write(chalk.cyan(delta.content));
|
|
67
|
+
}
|
|
68
|
+
chunkResponse += delta.content;
|
|
69
|
+
finalResponse += delta.content;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (delta?.tool_calls) {
|
|
73
|
+
if (spinner.isSpinning) spinner.stop();
|
|
74
|
+
for (const tc of delta.tool_calls) {
|
|
75
|
+
if (tc.index === undefined) continue;
|
|
76
|
+
if (!toolCalls[tc.index]) {
|
|
77
|
+
toolCalls[tc.index] = { id: tc.id, type: 'function', function: { name: tc.function.name, arguments: '' } };
|
|
78
|
+
}
|
|
79
|
+
if (tc.function?.arguments) {
|
|
80
|
+
toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
81
|
+
// Visual feedback for streaming tool arguments
|
|
82
|
+
if (!spinner.isSpinning) {
|
|
83
|
+
spinner = ora({ text: `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`, color: 'yellow', stream: process.stdout }).start();
|
|
84
|
+
} else {
|
|
85
|
+
spinner.text = `Generating ${chalk.yellow(toolCalls[tc.index].function.name)} (${toolCalls[tc.index].function.arguments.length} bytes)...`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (spinner.isSpinning) spinner.stop();
|
|
92
|
+
|
|
93
|
+
if (chunkResponse) {
|
|
94
|
+
if (this.config.useMarkedTerminal) printMarkdown(chunkResponse);
|
|
95
|
+
this.messages.push({ role: 'assistant', content: chunkResponse });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
toolCalls = toolCalls.filter(Boolean);
|
|
99
|
+
|
|
100
|
+
if (toolCalls.length === 0) {
|
|
101
|
+
console.log();
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.messages.push({
|
|
106
|
+
role: 'assistant',
|
|
107
|
+
tool_calls: toolCalls,
|
|
108
|
+
content: chunkResponse || null
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const call of toolCalls) {
|
|
112
|
+
console.log(chalk.yellow(`\n[Banana Calling Tool: ${call.function.name}]`));
|
|
113
|
+
let args = {};
|
|
114
|
+
try {
|
|
115
|
+
args = JSON.parse(call.function.arguments);
|
|
116
|
+
} catch (e) { }
|
|
117
|
+
|
|
118
|
+
const res = await executeTool(call.function.name, args, this.config);
|
|
119
|
+
if (this.config.debug) {
|
|
120
|
+
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk.yellow(`[Tool Result Received]\n`));
|
|
123
|
+
|
|
124
|
+
this.messages.push({
|
|
125
|
+
role: 'tool',
|
|
126
|
+
tool_call_id: call.id,
|
|
127
|
+
content: typeof res === 'string' ? res : JSON.stringify(res)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
spinner = ora({ text: 'Processing tool results...', color: 'yellow', stream: process.stdout }).start();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return finalResponse;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (spinner && spinner.isSpinning) spinner.stop();
|
|
137
|
+
console.error(chalk.red(`Mistral Runtime Error: ${err.message}`));
|
|
138
|
+
return `Error: ${err.message}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -47,7 +47,7 @@ export class OllamaCloudProvider {
|
|
|
47
47
|
model: this.modelName,
|
|
48
48
|
messages: this.messages,
|
|
49
49
|
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
50
|
-
stream:
|
|
50
|
+
stream: true
|
|
51
51
|
})
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -57,32 +57,61 @@ export class OllamaCloudProvider {
|
|
|
57
57
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
let currentChunkResponse = '';
|
|
63
|
+
let lastMessageObj = null;
|
|
64
|
+
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
|
|
69
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
70
|
+
const lines = chunk.split('\n').filter(l => l.trim() !== '');
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(line);
|
|
75
|
+
if (data.message) {
|
|
76
|
+
lastMessageObj = data.message;
|
|
77
|
+
if (data.message.content) {
|
|
78
|
+
const content = data.message.content;
|
|
79
|
+
if (spinner.isSpinning && !this.config.useMarkedTerminal) spinner.stop();
|
|
80
|
+
if (!this.config.useMarkedTerminal) {
|
|
81
|
+
process.stdout.write(chalk.cyan(content));
|
|
82
|
+
}
|
|
83
|
+
currentChunkResponse += content;
|
|
84
|
+
finalResponse += content;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Ignore partial JSON chunks
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
62
92
|
|
|
63
|
-
|
|
93
|
+
if (spinner.isSpinning) spinner.stop();
|
|
64
94
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
finalResponse += messageObj.content;
|
|
95
|
+
if (currentChunkResponse && this.config.useMarkedTerminal) {
|
|
96
|
+
printMarkdown(currentChunkResponse);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!lastMessageObj) {
|
|
100
|
+
throw new Error("Empty response from Ollama Cloud");
|
|
72
101
|
}
|
|
73
102
|
|
|
74
|
-
this.messages.push(
|
|
103
|
+
this.messages.push(lastMessageObj);
|
|
75
104
|
|
|
76
|
-
if (!
|
|
105
|
+
if (!lastMessageObj.tool_calls || lastMessageObj.tool_calls.length === 0) {
|
|
77
106
|
console.log();
|
|
78
107
|
break;
|
|
79
108
|
}
|
|
80
109
|
|
|
81
|
-
for (const call of
|
|
110
|
+
for (const call of lastMessageObj.tool_calls) {
|
|
82
111
|
const fn = call.function;
|
|
83
112
|
console.log(chalk.yellow(`\n[Banana Calling Tool: ${fn.name}]`));
|
|
84
113
|
|
|
85
|
-
let res = await executeTool(fn.name, fn.arguments);
|
|
114
|
+
let res = await executeTool(fn.name, fn.arguments, this.config);
|
|
86
115
|
if (this.config.debug) {
|
|
87
116
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
88
117
|
}
|
package/src/providers/openai.js
CHANGED
|
@@ -121,7 +121,7 @@ export class OpenAIProvider {
|
|
|
121
121
|
args = JSON.parse(call.function.arguments);
|
|
122
122
|
} catch (e) { }
|
|
123
123
|
|
|
124
|
-
const res = await executeTool(call.function.name, args);
|
|
124
|
+
const res = await executeTool(call.function.name, args, this.config);
|
|
125
125
|
if (this.config.debug) {
|
|
126
126
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
127
127
|
}
|
|
@@ -371,7 +371,7 @@ export class OpenAIProvider {
|
|
|
371
371
|
args = JSON.parse(call.function.arguments);
|
|
372
372
|
} catch (e) { }
|
|
373
373
|
|
|
374
|
-
const res = await executeTool(call.function.name, args);
|
|
374
|
+
const res = await executeTool(call.function.name, args, this.config);
|
|
375
375
|
if (this.config.debug) {
|
|
376
376
|
console.log(chalk.gray(`[DEBUG] Tool Result: ${typeof res === 'string' ? res : JSON.stringify(res, null, 2)}`));
|
|
377
377
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getAvailableTools, executeTool } from './registry.js';
|
|
4
|
+
import { getSystemPrompt } from '../prompt.js';
|
|
5
|
+
import { requestPermission } from '../permissions.js';
|
|
6
|
+
|
|
7
|
+
// Specialist prompts to guide sub-agents
|
|
8
|
+
const SPECIALIST_PROMPTS = {
|
|
9
|
+
researcher: "You are a Research Specialist. Your goal is to explore the codebase, find information, and answer specific questions. Do not make any file changes. Use tools like search_files, list_directory, and read_file to gather facts.",
|
|
10
|
+
coder: "You are a Coding Specialist. Your goal is to implement specific logic or fix bugs as requested. Focus on writing high-quality, idiomatic code using patch_file and write_file.",
|
|
11
|
+
reviewer: "You are a Code Reviewer. Your goal is to analyze provided code for bugs, security vulnerabilities, or style issues. Provide a detailed report of your findings.",
|
|
12
|
+
generalist: "You are a Generalist Sub-Agent. Complete the assigned task as efficiently as possible using all available tools."
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tool that allows the main agent to delegate a sub-task to a specialized agent.
|
|
17
|
+
*/
|
|
18
|
+
export async function delegateTask({ task, agentType = 'generalist', contextFiles = [] }, mainConfig) {
|
|
19
|
+
const perm = await requestPermission('Delegate Task', `${agentType} specialist: ${task}`);
|
|
20
|
+
if (!perm.allowed) {
|
|
21
|
+
return `User denied permission to delegate task to ${agentType} specialist.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const spinner = ora({
|
|
25
|
+
text: `Delegating to ${chalk.magenta(agentType)} specialist...`,
|
|
26
|
+
color: 'magenta',
|
|
27
|
+
stream: process.stdout
|
|
28
|
+
}).start();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 1. Setup the sub-agent config (inherit from main, but could be customized)
|
|
32
|
+
const subConfig = { ...mainConfig };
|
|
33
|
+
|
|
34
|
+
// Use a dynamic import to avoid circular dependency with index.js if needed,
|
|
35
|
+
// but here we can just manually create the provider based on current config.
|
|
36
|
+
const { GeminiProvider } = await import('../providers/gemini.js');
|
|
37
|
+
const { ClaudeProvider } = await import('../providers/claude.js');
|
|
38
|
+
const { OpenAIProvider } = await import('../providers/openai.js');
|
|
39
|
+
const { MistralProvider } = await import('../providers/mistral.js');
|
|
40
|
+
const { OllamaProvider } = await import('../providers/ollama.js');
|
|
41
|
+
const { OllamaCloudProvider } = await import('../providers/ollamaCloud.js');
|
|
42
|
+
|
|
43
|
+
const createSubProvider = (cfg) => {
|
|
44
|
+
switch (cfg.provider) {
|
|
45
|
+
case 'gemini': return new GeminiProvider(cfg);
|
|
46
|
+
case 'claude': return new ClaudeProvider(cfg);
|
|
47
|
+
case 'openai': return new OpenAIProvider(cfg);
|
|
48
|
+
case 'mistral': return new MistralProvider(cfg);
|
|
49
|
+
case 'ollama_cloud': return new OllamaCloudProvider(cfg);
|
|
50
|
+
case 'ollama': return new OllamaProvider(cfg);
|
|
51
|
+
default: return new OllamaProvider(cfg);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const subProvider = createSubProvider(subConfig);
|
|
56
|
+
|
|
57
|
+
// 2. Customize the system prompt for the specialist
|
|
58
|
+
const basePrompt = getSystemPrompt(subConfig);
|
|
59
|
+
const specialistInstruction = SPECIALIST_PROMPTS[agentType] || SPECIALIST_PROMPTS.generalist;
|
|
60
|
+
|
|
61
|
+
// Inject specialist instructions at the start
|
|
62
|
+
subProvider.updateSystemPrompt(`${specialistInstruction}\n\n${basePrompt}`);
|
|
63
|
+
|
|
64
|
+
// 3. Prepare initial message with context
|
|
65
|
+
let initialMessage = `TASK: ${task}`;
|
|
66
|
+
if (contextFiles.length > 0) {
|
|
67
|
+
const fs = await import('fs');
|
|
68
|
+
initialMessage += "\n\nCONTEXT FILES:";
|
|
69
|
+
for (const file of contextFiles) {
|
|
70
|
+
try {
|
|
71
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
72
|
+
initialMessage += `\n\n--- ${file} ---\n${content}`;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
initialMessage += `\n\n(Error reading context file ${file}: ${e.message})`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Run the sub-agent message loop
|
|
80
|
+
// We'll give it a limit of 5 turns to prevent infinite loops between agents
|
|
81
|
+
let turns = 0;
|
|
82
|
+
let finalResponse = '';
|
|
83
|
+
|
|
84
|
+
spinner.text = `Sub-agent (${agentType}) is working on the task...`;
|
|
85
|
+
|
|
86
|
+
// For the sub-agent, we want to capture its sendMessage result
|
|
87
|
+
// Note: Sub-agents run silently (their output isn't printed unless debug is on)
|
|
88
|
+
// to prevent terminal clutter.
|
|
89
|
+
finalResponse = await subProvider.sendMessage(initialMessage);
|
|
90
|
+
|
|
91
|
+
spinner.stop();
|
|
92
|
+
console.log(chalk.magenta(`[Sub-Agent ${agentType} task complete]`));
|
|
93
|
+
|
|
94
|
+
return `SUB-AGENT RESULT:\n${finalResponse}`;
|
|
95
|
+
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (spinner.isSpinning) spinner.stop();
|
|
98
|
+
return `Error in delegation: ${err.message}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/tools/registry.js
CHANGED
|
@@ -8,6 +8,7 @@ import { duckDuckGo } from './duckDuckGo.js';
|
|
|
8
8
|
import { duckDuckGoScrape } from './duckDuckGoScrape.js';
|
|
9
9
|
import { patchFile } from './patchFile.js';
|
|
10
10
|
import { activateSkill } from './activateSkill.js';
|
|
11
|
+
import { delegateTask } from './delegateTask.js';
|
|
11
12
|
|
|
12
13
|
export const TOOLS = [
|
|
13
14
|
{
|
|
@@ -145,6 +146,29 @@ export const TOOLS = [
|
|
|
145
146
|
},
|
|
146
147
|
required: ['skillName']
|
|
147
148
|
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'delegate_task',
|
|
152
|
+
label: 'Sub-Agent Delegation (Beta)',
|
|
153
|
+
description: 'Spawns a specialized sub-agent to handle a specific sub-task. Use this for complex research, big code changes, or detailed reviews to keep the main context clean.',
|
|
154
|
+
beta: true,
|
|
155
|
+
parameters: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
task: { type: 'string', description: 'The specific, detailed instruction for the sub-agent.' },
|
|
159
|
+
agentType: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: 'The type of specialist to spawn.',
|
|
162
|
+
enum: ['researcher', 'coder', 'reviewer', 'generalist']
|
|
163
|
+
},
|
|
164
|
+
contextFiles: {
|
|
165
|
+
type: 'array',
|
|
166
|
+
description: 'Optional list of file paths to provide as initial context to the sub-agent.',
|
|
167
|
+
items: { type: 'string' }
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
required: ['task']
|
|
171
|
+
}
|
|
148
172
|
}
|
|
149
173
|
];
|
|
150
174
|
|
|
@@ -157,7 +181,7 @@ export function getAvailableTools(config = {}) {
|
|
|
157
181
|
});
|
|
158
182
|
}
|
|
159
183
|
|
|
160
|
-
export async function executeTool(name, args) {
|
|
184
|
+
export async function executeTool(name, args, config) {
|
|
161
185
|
switch (name) {
|
|
162
186
|
case 'execute_command': return await execCommand(args);
|
|
163
187
|
case 'read_file': return await readFile(args);
|
|
@@ -169,6 +193,7 @@ export async function executeTool(name, args) {
|
|
|
169
193
|
case 'duck_duck_go_scrape': return await duckDuckGoScrape(args);
|
|
170
194
|
case 'patch_file': return await patchFile(args);
|
|
171
195
|
case 'activate_skill': return await activateSkill(args);
|
|
196
|
+
case 'delegate_task': return await delegateTask(args, config);
|
|
172
197
|
default: return `Unknown tool: ${name}`;
|
|
173
198
|
}
|
|
174
199
|
}
|