@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 +1 -1
- package/src/OllamaAgent.js +69 -26
- package/src/worker.js +3 -1
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
|
|
|
@@ -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(-
|
|
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(-
|
|
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
|
-
|
|
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
|