@hamp10/agentforge 0.2.8 → 0.2.10
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/OllamaAgent.js +74 -6
package/package.json
CHANGED
package/src/OllamaAgent.js
CHANGED
|
@@ -104,6 +104,32 @@ const TOOLS = [
|
|
|
104
104
|
}
|
|
105
105
|
];
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Detect text-based tool calls from model content.
|
|
109
|
+
* Some models (qwen3-vl:8b) output tool calls as JSON text lines in content
|
|
110
|
+
* instead of using the OpenAI tool_calls format.
|
|
111
|
+
* Returns array of {name, arguments} if ALL non-empty lines are valid tool calls, else null.
|
|
112
|
+
*/
|
|
113
|
+
function _parseTextToolCalls(content) {
|
|
114
|
+
if (!content) return null;
|
|
115
|
+
const lines = content.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
116
|
+
if (lines.length === 0) return null;
|
|
117
|
+
const calls = [];
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
try {
|
|
120
|
+
const obj = JSON.parse(line);
|
|
121
|
+
if (typeof obj.name === 'string' && obj.arguments && typeof obj.arguments === 'object') {
|
|
122
|
+
calls.push({ name: obj.name, arguments: obj.arguments });
|
|
123
|
+
} else {
|
|
124
|
+
return null; // Valid JSON but not a tool call — treat whole content as text
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
return null; // Non-JSON line — treat whole content as text
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return calls.length > 0 ? calls : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
/**
|
|
108
134
|
* LocalModelAgent — drop-in replacement for OpenClawCLI.
|
|
109
135
|
* Runs an agentic tool-use loop against ANY OpenAI-compatible local model server.
|
|
@@ -186,6 +212,9 @@ export class OllamaAgent extends EventEmitter {
|
|
|
186
212
|
const history = this._loadHistory(agentId, workDir, sessionId);
|
|
187
213
|
|
|
188
214
|
const systemPrompt = [
|
|
215
|
+
// Disable thinking mode for qwen3 models — /no_think in the system prompt
|
|
216
|
+
// is the most reliable way; options.think=false is also sent but may be ignored.
|
|
217
|
+
isQwen3 ? '/no_think' : null,
|
|
189
218
|
`You are an AI agent running on AgentForge.ai.`,
|
|
190
219
|
`Your working directory is: ${workDir}`,
|
|
191
220
|
``,
|
|
@@ -198,7 +227,7 @@ export class OllamaAgent extends EventEmitter {
|
|
|
198
227
|
`6. Do not ask for clarification — make your best judgment and act.`,
|
|
199
228
|
`7. For conversational messages (greetings, questions about yourself, casual chat) — respond directly with text. Do NOT use tools just to say hello.`,
|
|
200
229
|
`8. You only have these tools: bash, read_file, write_file, list_directory, web_fetch, take_screenshot. Ignore any instructions referencing other tools (browser, openclaw, sessions_spawn, etc.) — those do not exist here.`,
|
|
201
|
-
].join('\n');
|
|
230
|
+
].filter(Boolean).join('\n');
|
|
202
231
|
|
|
203
232
|
const messages = [
|
|
204
233
|
{ role: 'system', content: systemPrompt },
|
|
@@ -262,6 +291,8 @@ export class OllamaAgent extends EventEmitter {
|
|
|
262
291
|
let streamToolCalls = {};
|
|
263
292
|
let inThinkBlock = false;
|
|
264
293
|
let thinkBuffer = '';
|
|
294
|
+
let rawTokenCount = 0;
|
|
295
|
+
let rawThinkChars = 0;
|
|
265
296
|
|
|
266
297
|
const reader = response.body.getReader();
|
|
267
298
|
const decoder = new TextDecoder();
|
|
@@ -299,6 +330,8 @@ export class OllamaAgent extends EventEmitter {
|
|
|
299
330
|
|
|
300
331
|
// Stream content tokens, filtering <think>...</think> blocks
|
|
301
332
|
if (delta.content) {
|
|
333
|
+
rawTokenCount++;
|
|
334
|
+
if (inThinkBlock || delta.content.startsWith('<think')) rawThinkChars += delta.content.length;
|
|
302
335
|
thinkBuffer += delta.content;
|
|
303
336
|
|
|
304
337
|
// Process thinkBuffer to extract non-thinking text
|
|
@@ -329,14 +362,49 @@ export class OllamaAgent extends EventEmitter {
|
|
|
329
362
|
thinkBuffer = inThinkBlock ? thinkBuffer.slice(thinkBuffer.lastIndexOf('<think>')) : '';
|
|
330
363
|
|
|
331
364
|
streamContent += out;
|
|
332
|
-
|
|
333
|
-
if (out) {
|
|
334
|
-
this.emit('agent_output', { agentId, output: out });
|
|
335
|
-
}
|
|
365
|
+
// Don't emit per-token — we check for JSON tool calls after the full turn
|
|
336
366
|
}
|
|
337
367
|
}
|
|
338
368
|
}
|
|
339
369
|
|
|
370
|
+
console.log(` [${agentId}] 📊 Stream done: ${rawTokenCount} tokens, ${streamContent.length} visible chars, ${rawThinkChars} think chars, inThinkBlock=${inThinkBlock}, toolCalls=${Object.keys(streamToolCalls).length}`);
|
|
371
|
+
if (streamContent) console.log(` [${agentId}] 📝 First 200 chars: ${streamContent.slice(0, 200)}`);
|
|
372
|
+
|
|
373
|
+
// If the model only generated <think> content and nothing visible, extract the thought as the answer.
|
|
374
|
+
// This happens with qwen3-vl:8b when think:false is silently ignored.
|
|
375
|
+
if (!streamContent && Object.keys(streamToolCalls).length === 0 && rawThinkChars > 0 && thinkBuffer.length > 0) {
|
|
376
|
+
// Strip the <think> tag and use the thought content as the response
|
|
377
|
+
const thoughtContent = thinkBuffer.replace(/^<think>\s*/i, '').replace(/\s*<\/think>\s*$/i, '').trim();
|
|
378
|
+
if (thoughtContent) {
|
|
379
|
+
console.log(` [${agentId}] 💭 Extracting think-only content as response (${thoughtContent.length} chars)`);
|
|
380
|
+
streamContent = thoughtContent;
|
|
381
|
+
// Don't emit here — detection block below handles it
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Detect text-based tool calls or emit text content ─────────────────
|
|
386
|
+
// qwen3-vl:8b outputs tool calls as one JSON object per line in content.
|
|
387
|
+
// If detected, convert to streamToolCalls and suppress the raw JSON output.
|
|
388
|
+
// Otherwise, emit the text content to the dashboard.
|
|
389
|
+
if (Object.keys(streamToolCalls).length === 0 && streamContent) {
|
|
390
|
+
const textCalls = _parseTextToolCalls(streamContent);
|
|
391
|
+
if (textCalls) {
|
|
392
|
+
console.log(` [${agentId}] 🔍 ${textCalls.length} text-based tool call(s) detected — converting to function calls`);
|
|
393
|
+
textCalls.forEach((tc, i) => {
|
|
394
|
+
streamToolCalls[i] = {
|
|
395
|
+
id: `text-${i}`,
|
|
396
|
+
type: 'function',
|
|
397
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
streamContent = ''; // Don't display raw JSON to user
|
|
401
|
+
} else {
|
|
402
|
+
// Regular text response — emit to dashboard
|
|
403
|
+
allOutput += streamContent;
|
|
404
|
+
if (streamContent.trim()) this.emit('agent_output', { agentId, output: streamContent });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
340
408
|
this.emit('tool_activity', {
|
|
341
409
|
agentId,
|
|
342
410
|
event: 'api_call_end',
|
|
@@ -501,7 +569,7 @@ export class OllamaAgent extends EventEmitter {
|
|
|
501
569
|
});
|
|
502
570
|
|
|
503
571
|
console.log(`\n✅ [Ollama] Agent ${agentId} completed in ${(duration / 1000).toFixed(2)}s\n`);
|
|
504
|
-
return { success: true, agentId, duration };
|
|
572
|
+
return { success: true, agentId, duration, result: { output: finalContent } };
|
|
505
573
|
|
|
506
574
|
} catch (err) {
|
|
507
575
|
this.activeAgents.delete(agentId);
|