groove-dev 0.27.150 → 0.27.151

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -24,6 +24,11 @@ export class AgentLoop extends EventEmitter {
24
24
  this.idle = true;
25
25
  this.abortController = null;
26
26
 
27
+ // Tool calling mode: 'native' uses OpenAI function-calling API fields,
28
+ // 'prompt' injects tool schemas into the system prompt and parses
29
+ // <tool_call> blocks from the model's text output.
30
+ this.toolMode = 'native';
31
+
27
32
  // Metrics
28
33
  this.totalTokensIn = 0;
29
34
  this.totalTokensOut = 0;
@@ -132,7 +137,7 @@ export class AgentLoop extends EventEmitter {
132
137
  const response = await this._callApi();
133
138
  if (!response || !this.running) break;
134
139
 
135
- const { content, toolCalls, usage, finishReason } = response;
140
+ let { content, toolCalls, usage, finishReason } = response;
136
141
  consecutiveErrors = 0; // Reset on successful call
137
142
 
138
143
  // Update token tracking from API response
@@ -140,10 +145,18 @@ export class AgentLoop extends EventEmitter {
140
145
  this._updateTokens(usage);
141
146
  }
142
147
 
148
+ // In prompt-based mode, parse tool calls from the model's text
149
+ if (this.toolMode === 'prompt' && content) {
150
+ const parsed = this._parseToolCallsFromText(content);
151
+ if (parsed.length > 0) {
152
+ toolCalls = parsed;
153
+ }
154
+ }
155
+
143
156
  // Append assistant message to conversation history
144
157
  const assistantMsg = { role: 'assistant' };
145
158
  if (content) assistantMsg.content = content;
146
- if (toolCalls?.length > 0) {
159
+ if (this.toolMode === 'native' && toolCalls?.length > 0) {
147
160
  assistantMsg.tool_calls = toolCalls.map((tc) => ({
148
161
  id: tc.id,
149
162
  type: 'function',
@@ -162,9 +175,12 @@ export class AgentLoop extends EventEmitter {
162
175
  }
163
176
 
164
177
  // Has tool calls — broadcast text before executing tools (if model sent text + tools)
165
- if (content) {
166
- this._writeLog({ type: 'assistant', content: content.slice(0, 2000) });
167
- this.emit('output', { type: 'activity', subtype: 'assistant', data: content });
178
+ const displayContent = this.toolMode === 'prompt'
179
+ ? (content || '').replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim()
180
+ : content;
181
+ if (displayContent) {
182
+ this._writeLog({ type: 'assistant', content: displayContent.slice(0, 2000) });
183
+ this.emit('output', { type: 'activity', subtype: 'assistant', data: displayContent });
168
184
  }
169
185
 
170
186
  // Execute each tool call
@@ -211,11 +227,19 @@ export class AgentLoop extends EventEmitter {
211
227
  }
212
228
 
213
229
  // Append tool result to conversation for the model
214
- this.messages.push({
215
- role: 'tool',
216
- tool_call_id: call.id,
217
- content: result.success ? (result.result || 'Done.') : `Error: ${result.error}`,
218
- });
230
+ const resultContent = result.success ? (result.result || 'Done.') : `Error: ${result.error}`;
231
+ if (this.toolMode === 'native') {
232
+ this.messages.push({
233
+ role: 'tool',
234
+ tool_call_id: call.id,
235
+ content: resultContent,
236
+ });
237
+ } else {
238
+ this.messages.push({
239
+ role: 'user',
240
+ content: `<tool_result name="${toolName}">\n${resultContent}\n</tool_result>`,
241
+ });
242
+ }
219
243
  }
220
244
 
221
245
  // Context rotation is handled by the Rotator's 15s polling loop
@@ -236,12 +260,15 @@ export class AgentLoop extends EventEmitter {
236
260
  const body = {
237
261
  model: this.config.model,
238
262
  messages: this.messages,
239
- tools: TOOL_DEFINITIONS,
240
- tool_choice: 'auto',
241
263
  temperature: this.config.temperature ?? 0.1,
242
264
  max_tokens: this.config.maxResponseTokens || 4096,
243
265
  };
244
266
 
267
+ if (this.toolMode === 'native') {
268
+ body.tools = TOOL_DEFINITIONS;
269
+ body.tool_choice = 'auto';
270
+ }
271
+
245
272
  if (this.config.stream !== false) {
246
273
  body.stream = true;
247
274
  body.stream_options = { include_usage: true };
@@ -283,6 +310,18 @@ export class AgentLoop extends EventEmitter {
283
310
  const text = await response.text().catch(() => '');
284
311
  const errMsg = `API error ${response.status}: ${text.slice(0, 500)}`;
285
312
 
313
+ // Detect tool_choice rejection (vLLM, TGI, etc. without tool-calling flags)
314
+ // Fall back to prompt-based tool calling and retry immediately
315
+ if (response.status === 400 && this.toolMode === 'native' &&
316
+ (text.includes('tool_choice') || text.includes('tool-call-parser') || text.includes('enable-auto-tool-choice'))) {
317
+ this._writeLog({ type: 'system', event: 'tool-fallback', reason: 'Runtime rejected native tool calling — switching to prompt-based tools' });
318
+ this.toolMode = 'prompt';
319
+ this._injectToolPrompt();
320
+ delete body.tools;
321
+ delete body.tool_choice;
322
+ continue;
323
+ }
324
+
286
325
  if (response.status === 401 || response.status === 403) {
287
326
  this._writeLog({ type: 'error', text: errMsg });
288
327
  this.emit('error', { message: errMsg });
@@ -405,6 +444,65 @@ export class AgentLoop extends EventEmitter {
405
444
  };
406
445
  }
407
446
 
447
+ // --- Prompt-Based Tool Calling Fallback ---
448
+
449
+ _injectToolPrompt() {
450
+ const toolPrompt = this._buildToolPrompt();
451
+ const systemIdx = this.messages.findIndex(m => m.role === 'system');
452
+ if (systemIdx >= 0) {
453
+ this.messages[systemIdx].content += '\n\n' + toolPrompt;
454
+ } else {
455
+ this.messages.unshift({ role: 'system', content: toolPrompt });
456
+ }
457
+ }
458
+
459
+ _buildToolPrompt() {
460
+ const toolDefs = TOOL_DEFINITIONS.map(t => {
461
+ const f = t.function;
462
+ const params = Object.entries(f.parameters.properties).map(([name, schema]) => {
463
+ const req = f.parameters.required?.includes(name) ? ' (required)' : ' (optional)';
464
+ return ` - ${name}: ${schema.type}${req} — ${schema.description}`;
465
+ }).join('\n');
466
+ return `### ${f.name}\n${f.description}\nParameters:\n${params}`;
467
+ }).join('\n\n');
468
+
469
+ return `## Available Tools
470
+
471
+ To use a tool, include a tool_call block in your response:
472
+
473
+ <tool_call>
474
+ {"name": "tool_name", "arguments": {"param1": "value1"}}
475
+ </tool_call>
476
+
477
+ You can make multiple tool calls in one response. After each tool call you will receive a <tool_result> with the output.
478
+
479
+ ${toolDefs}
480
+
481
+ Always use tools to read, write, or search files and to run commands. Do not guess file contents.`;
482
+ }
483
+
484
+ _parseToolCallsFromText(content) {
485
+ if (!content) return [];
486
+ const calls = [];
487
+ const regex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
488
+ let match;
489
+ while ((match = regex.exec(content)) !== null) {
490
+ try {
491
+ const parsed = JSON.parse(match[1].trim());
492
+ if (parsed.name) {
493
+ calls.push({
494
+ id: `call_${Date.now()}_${calls.length}`,
495
+ function: {
496
+ name: parsed.name,
497
+ arguments: JSON.stringify(parsed.arguments || {}),
498
+ },
499
+ });
500
+ }
501
+ } catch { /* skip malformed tool call */ }
502
+ }
503
+ return calls;
504
+ }
505
+
408
506
  // --- Token Tracking ---
409
507
 
410
508
  _updateTokens(usage) {
@@ -137,6 +137,7 @@ export class LocalProvider extends Provider {
137
137
  let model = agent.model || 'qwen2.5-coder:7b';
138
138
  let apiBase = 'http://localhost:11434/v1';
139
139
  let apiKey = agent.apiKey || null;
140
+ let runtimeType = 'ollama';
140
141
 
141
142
  if (agent.apiBase) {
142
143
  apiBase = agent.apiBase;
@@ -153,6 +154,7 @@ export class LocalProvider extends Provider {
153
154
  if (rt) {
154
155
  apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
155
156
  if (rt.apiKey) apiKey = rt.apiKey;
157
+ if (rt.type) runtimeType = rt.type;
156
158
  const rtModel = rt.models?.[0];
157
159
  model = rtModel?.id || rtModel?.name || ggufId;
158
160
  }
@@ -167,6 +169,7 @@ export class LocalProvider extends Provider {
167
169
  if (rt) {
168
170
  apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
169
171
  if (rt.apiKey) apiKey = rt.apiKey;
172
+ if (rt.type) runtimeType = rt.type;
170
173
  model = modelId;
171
174
  }
172
175
  }
@@ -176,6 +179,7 @@ export class LocalProvider extends Provider {
176
179
  return {
177
180
  apiBase,
178
181
  model,
182
+ runtimeType,
179
183
  contextWindow,
180
184
  temperature: typeof agent.temperature === 'number' ? agent.temperature : 0.1,
181
185
  maxResponseTokens: 4096,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -24,6 +24,11 @@ export class AgentLoop extends EventEmitter {
24
24
  this.idle = true;
25
25
  this.abortController = null;
26
26
 
27
+ // Tool calling mode: 'native' uses OpenAI function-calling API fields,
28
+ // 'prompt' injects tool schemas into the system prompt and parses
29
+ // <tool_call> blocks from the model's text output.
30
+ this.toolMode = 'native';
31
+
27
32
  // Metrics
28
33
  this.totalTokensIn = 0;
29
34
  this.totalTokensOut = 0;
@@ -132,7 +137,7 @@ export class AgentLoop extends EventEmitter {
132
137
  const response = await this._callApi();
133
138
  if (!response || !this.running) break;
134
139
 
135
- const { content, toolCalls, usage, finishReason } = response;
140
+ let { content, toolCalls, usage, finishReason } = response;
136
141
  consecutiveErrors = 0; // Reset on successful call
137
142
 
138
143
  // Update token tracking from API response
@@ -140,10 +145,18 @@ export class AgentLoop extends EventEmitter {
140
145
  this._updateTokens(usage);
141
146
  }
142
147
 
148
+ // In prompt-based mode, parse tool calls from the model's text
149
+ if (this.toolMode === 'prompt' && content) {
150
+ const parsed = this._parseToolCallsFromText(content);
151
+ if (parsed.length > 0) {
152
+ toolCalls = parsed;
153
+ }
154
+ }
155
+
143
156
  // Append assistant message to conversation history
144
157
  const assistantMsg = { role: 'assistant' };
145
158
  if (content) assistantMsg.content = content;
146
- if (toolCalls?.length > 0) {
159
+ if (this.toolMode === 'native' && toolCalls?.length > 0) {
147
160
  assistantMsg.tool_calls = toolCalls.map((tc) => ({
148
161
  id: tc.id,
149
162
  type: 'function',
@@ -162,9 +175,12 @@ export class AgentLoop extends EventEmitter {
162
175
  }
163
176
 
164
177
  // Has tool calls — broadcast text before executing tools (if model sent text + tools)
165
- if (content) {
166
- this._writeLog({ type: 'assistant', content: content.slice(0, 2000) });
167
- this.emit('output', { type: 'activity', subtype: 'assistant', data: content });
178
+ const displayContent = this.toolMode === 'prompt'
179
+ ? (content || '').replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim()
180
+ : content;
181
+ if (displayContent) {
182
+ this._writeLog({ type: 'assistant', content: displayContent.slice(0, 2000) });
183
+ this.emit('output', { type: 'activity', subtype: 'assistant', data: displayContent });
168
184
  }
169
185
 
170
186
  // Execute each tool call
@@ -211,11 +227,19 @@ export class AgentLoop extends EventEmitter {
211
227
  }
212
228
 
213
229
  // Append tool result to conversation for the model
214
- this.messages.push({
215
- role: 'tool',
216
- tool_call_id: call.id,
217
- content: result.success ? (result.result || 'Done.') : `Error: ${result.error}`,
218
- });
230
+ const resultContent = result.success ? (result.result || 'Done.') : `Error: ${result.error}`;
231
+ if (this.toolMode === 'native') {
232
+ this.messages.push({
233
+ role: 'tool',
234
+ tool_call_id: call.id,
235
+ content: resultContent,
236
+ });
237
+ } else {
238
+ this.messages.push({
239
+ role: 'user',
240
+ content: `<tool_result name="${toolName}">\n${resultContent}\n</tool_result>`,
241
+ });
242
+ }
219
243
  }
220
244
 
221
245
  // Context rotation is handled by the Rotator's 15s polling loop
@@ -236,12 +260,15 @@ export class AgentLoop extends EventEmitter {
236
260
  const body = {
237
261
  model: this.config.model,
238
262
  messages: this.messages,
239
- tools: TOOL_DEFINITIONS,
240
- tool_choice: 'auto',
241
263
  temperature: this.config.temperature ?? 0.1,
242
264
  max_tokens: this.config.maxResponseTokens || 4096,
243
265
  };
244
266
 
267
+ if (this.toolMode === 'native') {
268
+ body.tools = TOOL_DEFINITIONS;
269
+ body.tool_choice = 'auto';
270
+ }
271
+
245
272
  if (this.config.stream !== false) {
246
273
  body.stream = true;
247
274
  body.stream_options = { include_usage: true };
@@ -283,6 +310,18 @@ export class AgentLoop extends EventEmitter {
283
310
  const text = await response.text().catch(() => '');
284
311
  const errMsg = `API error ${response.status}: ${text.slice(0, 500)}`;
285
312
 
313
+ // Detect tool_choice rejection (vLLM, TGI, etc. without tool-calling flags)
314
+ // Fall back to prompt-based tool calling and retry immediately
315
+ if (response.status === 400 && this.toolMode === 'native' &&
316
+ (text.includes('tool_choice') || text.includes('tool-call-parser') || text.includes('enable-auto-tool-choice'))) {
317
+ this._writeLog({ type: 'system', event: 'tool-fallback', reason: 'Runtime rejected native tool calling — switching to prompt-based tools' });
318
+ this.toolMode = 'prompt';
319
+ this._injectToolPrompt();
320
+ delete body.tools;
321
+ delete body.tool_choice;
322
+ continue;
323
+ }
324
+
286
325
  if (response.status === 401 || response.status === 403) {
287
326
  this._writeLog({ type: 'error', text: errMsg });
288
327
  this.emit('error', { message: errMsg });
@@ -405,6 +444,65 @@ export class AgentLoop extends EventEmitter {
405
444
  };
406
445
  }
407
446
 
447
+ // --- Prompt-Based Tool Calling Fallback ---
448
+
449
+ _injectToolPrompt() {
450
+ const toolPrompt = this._buildToolPrompt();
451
+ const systemIdx = this.messages.findIndex(m => m.role === 'system');
452
+ if (systemIdx >= 0) {
453
+ this.messages[systemIdx].content += '\n\n' + toolPrompt;
454
+ } else {
455
+ this.messages.unshift({ role: 'system', content: toolPrompt });
456
+ }
457
+ }
458
+
459
+ _buildToolPrompt() {
460
+ const toolDefs = TOOL_DEFINITIONS.map(t => {
461
+ const f = t.function;
462
+ const params = Object.entries(f.parameters.properties).map(([name, schema]) => {
463
+ const req = f.parameters.required?.includes(name) ? ' (required)' : ' (optional)';
464
+ return ` - ${name}: ${schema.type}${req} — ${schema.description}`;
465
+ }).join('\n');
466
+ return `### ${f.name}\n${f.description}\nParameters:\n${params}`;
467
+ }).join('\n\n');
468
+
469
+ return `## Available Tools
470
+
471
+ To use a tool, include a tool_call block in your response:
472
+
473
+ <tool_call>
474
+ {"name": "tool_name", "arguments": {"param1": "value1"}}
475
+ </tool_call>
476
+
477
+ You can make multiple tool calls in one response. After each tool call you will receive a <tool_result> with the output.
478
+
479
+ ${toolDefs}
480
+
481
+ Always use tools to read, write, or search files and to run commands. Do not guess file contents.`;
482
+ }
483
+
484
+ _parseToolCallsFromText(content) {
485
+ if (!content) return [];
486
+ const calls = [];
487
+ const regex = /<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/g;
488
+ let match;
489
+ while ((match = regex.exec(content)) !== null) {
490
+ try {
491
+ const parsed = JSON.parse(match[1].trim());
492
+ if (parsed.name) {
493
+ calls.push({
494
+ id: `call_${Date.now()}_${calls.length}`,
495
+ function: {
496
+ name: parsed.name,
497
+ arguments: JSON.stringify(parsed.arguments || {}),
498
+ },
499
+ });
500
+ }
501
+ } catch { /* skip malformed tool call */ }
502
+ }
503
+ return calls;
504
+ }
505
+
408
506
  // --- Token Tracking ---
409
507
 
410
508
  _updateTokens(usage) {
@@ -137,6 +137,7 @@ export class LocalProvider extends Provider {
137
137
  let model = agent.model || 'qwen2.5-coder:7b';
138
138
  let apiBase = 'http://localhost:11434/v1';
139
139
  let apiKey = agent.apiKey || null;
140
+ let runtimeType = 'ollama';
140
141
 
141
142
  if (agent.apiBase) {
142
143
  apiBase = agent.apiBase;
@@ -153,6 +154,7 @@ export class LocalProvider extends Provider {
153
154
  if (rt) {
154
155
  apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
155
156
  if (rt.apiKey) apiKey = rt.apiKey;
157
+ if (rt.type) runtimeType = rt.type;
156
158
  const rtModel = rt.models?.[0];
157
159
  model = rtModel?.id || rtModel?.name || ggufId;
158
160
  }
@@ -167,6 +169,7 @@ export class LocalProvider extends Provider {
167
169
  if (rt) {
168
170
  apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
169
171
  if (rt.apiKey) apiKey = rt.apiKey;
172
+ if (rt.type) runtimeType = rt.type;
170
173
  model = modelId;
171
174
  }
172
175
  }
@@ -176,6 +179,7 @@ export class LocalProvider extends Provider {
176
179
  return {
177
180
  apiBase,
178
181
  model,
182
+ runtimeType,
179
183
  contextWindow,
180
184
  temperature: typeof agent.temperature === 'number' ? agent.temperature : 0.1,
181
185
  maxResponseTokens: 4096,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.150",
3
+ "version": "0.27.151",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",