@hamp10/agentforge 0.2.12 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamp10/agentforge",
3
- "version": "0.2.12",
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
 
@@ -781,7 +824,7 @@ export class OllamaAgent extends EventEmitter {
781
824
  if (existsSync(fp)) {
782
825
  const data = JSON.parse(readFileSync(fp, 'utf-8'));
783
826
  // Keep last 20 messages to stay within context
784
- return data.slice(-20);
827
+ return data.slice(-12);
785
828
  }
786
829
  } catch {}
787
830
  return [];
@@ -790,7 +833,7 @@ export class OllamaAgent extends EventEmitter {
790
833
  _saveHistory(agentId, workDir, sessionId, messages) {
791
834
  try {
792
835
  const fp = this._historyPath(workDir, sessionId);
793
- writeFileSync(fp, JSON.stringify(messages.slice(-40), null, 2));
836
+ writeFileSync(fp, JSON.stringify(messages.slice(-20), null, 2));
794
837
  } catch {}
795
838
  }
796
839
  }
package/src/worker.js CHANGED
@@ -1206,7 +1206,9 @@ export class AgentForgeWorker extends EventEmitter {
1206
1206
  // Only inject history when gateway is unavailable (subprocess fallback).
1207
1207
  // Hampagent manages its own session history natively — never inject DB history for it
1208
1208
  const gatewayActive = !!(this.cli.gatewayPort && this.cli.gatewayToken);
1209
- const sessionExists = useHampagent || gatewayActive;
1209
+ // OllamaAgent manages its own disk-based session history natively —
1210
+ // injecting DB history as text would double the context and overflow the model.
1211
+ const sessionExists = useHampagent || gatewayActive || isLocalModelRunner;
1210
1212
 
1211
1213
  if (!sessionExists) {
1212
1214
  // Session is gone — inject DB history as context prefix so the agent remembers