@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/OllamaAgent.js +67 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "AgentForge worker — connect your machine to agentforge.ai",
5
5
  "type": "module",
6
6
  "bin": {
@@ -110,26 +110,65 @@ const TOOLS = [
110
110
 
111
111
  /**
112
112
  * Detect text-based tool calls from model content.
113
- * Some models (qwen3-vl:8b) output tool calls as JSON text lines in content
114
- * instead of using the OpenAI tool_calls format.
115
- * Returns array of {name, arguments} if ALL non-empty lines are valid tool calls, else null.
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 lines = content.trim().split('\n').map(l => l.trim()).filter(Boolean);
120
- if (lines.length === 0) return null;
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
- for (const line of lines) {
123
- try {
124
- const obj = JSON.parse(line);
125
- if (typeof obj.name === 'string' && obj.arguments && typeof obj.arguments === 'object') {
126
- calls.push({ name: obj.name, arguments: obj.arguments });
127
- } else {
128
- return null; // Valid JSON but not a tool call — treat whole content as text
129
- }
130
- } catch {
131
- return null; // Non-JSON line — treat whole content as text
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: 'api_call_start', description: `🦙 Calling ${effectiveModel}...` });
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
- // Don't emit per-tokenwe check for JSON tool calls after the full turn
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 emit text content ─────────────────
390
- // qwen3-vl:8b outputs tool calls as one JSON object per line in content.
391
- // If detected, convert to streamToolCalls and suppress the raw JSON output.
392
- // Otherwise, emit the text content to the dashboard.
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 = ''; // Don't display raw JSON to user
448
+ streamContent = ''; // Suppress raw JSON from output
405
449
  } else {
406
- // Regular text response emit to dashboard
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