@hamp10/agentforge 0.2.13 → 0.2.14
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 +67 -24
package/package.json
CHANGED
package/src/OllamaAgent.js
CHANGED
|
@@ -110,26 +110,65 @@ const TOOLS = [
|
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* Detect text-based tool calls from model content.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* qwen3-vl:8b outputs tool calls as JSON in content rather than tool_calls field.
|
|
114
|
+
* Supports two schemas:
|
|
115
|
+
* - {name, arguments} (OpenAI-style)
|
|
116
|
+
* - {tool, args} (qwen3 native style)
|
|
117
|
+
* Supports both compact (one JSON per line) and pretty-printed multi-line JSON blocks.
|
|
118
|
+
* Returns array of {name, arguments} if content is ONLY tool calls, else null.
|
|
116
119
|
*/
|
|
117
120
|
function _parseTextToolCalls(content) {
|
|
118
121
|
if (!content) return null;
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
122
|
+
const trimmed = content.trim();
|
|
123
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null;
|
|
124
|
+
|
|
125
|
+
// Normalise a single parsed object into {name, arguments}
|
|
126
|
+
const normalise = (obj) => {
|
|
127
|
+
if (typeof obj.name === 'string' && obj.arguments !== undefined) {
|
|
128
|
+
const args = typeof obj.arguments === 'string' ? JSON.parse(obj.arguments) : obj.arguments;
|
|
129
|
+
return { name: obj.name, arguments: args };
|
|
130
|
+
}
|
|
131
|
+
if (typeof obj.tool === 'string' && obj.args !== undefined) {
|
|
132
|
+
return { name: obj.tool, arguments: obj.args };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Try parsing the whole content as a single JSON object/array
|
|
138
|
+
try {
|
|
139
|
+
const obj = JSON.parse(trimmed);
|
|
140
|
+
if (Array.isArray(obj)) {
|
|
141
|
+
const calls = obj.map(normalise);
|
|
142
|
+
if (calls.every(Boolean)) return calls;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const call = normalise(obj);
|
|
146
|
+
if (call) return [call];
|
|
147
|
+
return null;
|
|
148
|
+
} catch {}
|
|
149
|
+
|
|
150
|
+
// Try extracting multiple top-level JSON objects (separated by newlines/whitespace)
|
|
121
151
|
const calls = [];
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
152
|
+
let i = 0;
|
|
153
|
+
while (i < trimmed.length) {
|
|
154
|
+
// Skip whitespace/newlines between objects
|
|
155
|
+
while (i < trimmed.length && /\s/.test(trimmed[i])) i++;
|
|
156
|
+
if (i >= trimmed.length) break;
|
|
157
|
+
if (trimmed[i] !== '{') return null; // Non-JSON between objects — bail
|
|
158
|
+
// Find matching closing brace
|
|
159
|
+
let depth = 0, j = i;
|
|
160
|
+
while (j < trimmed.length) {
|
|
161
|
+
if (trimmed[j] === '{') depth++;
|
|
162
|
+
else if (trimmed[j] === '}') { depth--; if (depth === 0) { j++; break; } }
|
|
163
|
+
j++;
|
|
132
164
|
}
|
|
165
|
+
try {
|
|
166
|
+
const obj = JSON.parse(trimmed.slice(i, j));
|
|
167
|
+
const call = normalise(obj);
|
|
168
|
+
if (!call) return null;
|
|
169
|
+
calls.push(call);
|
|
170
|
+
i = j;
|
|
171
|
+
} catch { return null; }
|
|
133
172
|
}
|
|
134
173
|
return calls.length > 0 ? calls : null;
|
|
135
174
|
}
|
|
@@ -254,7 +293,7 @@ export class OllamaAgent extends EventEmitter {
|
|
|
254
293
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
255
294
|
if (controller.signal.aborted) break;
|
|
256
295
|
|
|
257
|
-
this.emit('tool_activity', { agentId, event: '
|
|
296
|
+
this.emit('tool_activity', { agentId, event: 'tool_start', tool: 'model', description: `Thinking…` });
|
|
258
297
|
|
|
259
298
|
let response;
|
|
260
299
|
try {
|
|
@@ -366,7 +405,12 @@ export class OllamaAgent extends EventEmitter {
|
|
|
366
405
|
thinkBuffer = inThinkBlock ? thinkBuffer.slice(thinkBuffer.lastIndexOf('<think>')) : '';
|
|
367
406
|
|
|
368
407
|
streamContent += out;
|
|
369
|
-
//
|
|
408
|
+
// Stream text tokens live — but only if output clearly isn't JSON tool calls.
|
|
409
|
+
// If the accumulated content starts with '{', it may be a tool call — buffer silently.
|
|
410
|
+
// Otherwise emit immediately so the user sees live output.
|
|
411
|
+
if (out && !streamContent.trimStart().startsWith('{')) {
|
|
412
|
+
this.emit('agent_output', { agentId, output: out, isChunk: true });
|
|
413
|
+
}
|
|
370
414
|
}
|
|
371
415
|
}
|
|
372
416
|
}
|
|
@@ -386,10 +430,10 @@ export class OllamaAgent extends EventEmitter {
|
|
|
386
430
|
}
|
|
387
431
|
}
|
|
388
432
|
|
|
389
|
-
// ── Detect text-based tool calls or
|
|
390
|
-
// qwen3-vl:8b outputs tool calls as
|
|
391
|
-
// If detected, convert to streamToolCalls
|
|
392
|
-
//
|
|
433
|
+
// ── Detect text-based tool calls or accumulate text content ──────────
|
|
434
|
+
// qwen3-vl:8b outputs tool calls as JSON in content (not tool_calls field).
|
|
435
|
+
// If detected, convert to streamToolCalls so they actually execute.
|
|
436
|
+
// If not tool calls, content was already streamed live token-by-token above.
|
|
393
437
|
if (Object.keys(streamToolCalls).length === 0 && streamContent) {
|
|
394
438
|
const textCalls = _parseTextToolCalls(streamContent);
|
|
395
439
|
if (textCalls) {
|
|
@@ -401,11 +445,10 @@ export class OllamaAgent extends EventEmitter {
|
|
|
401
445
|
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
402
446
|
};
|
|
403
447
|
});
|
|
404
|
-
streamContent = ''; //
|
|
448
|
+
streamContent = ''; // Suppress raw JSON from output
|
|
405
449
|
} else {
|
|
406
|
-
// Regular text
|
|
450
|
+
// Regular text — already emitted live above, just accumulate
|
|
407
451
|
allOutput += streamContent;
|
|
408
|
-
if (streamContent.trim()) this.emit('agent_output', { agentId, output: streamContent });
|
|
409
452
|
}
|
|
410
453
|
}
|
|
411
454
|
|