@krmxd/onegpt 0.2.2-beta → 0.2.4-beta
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/agent.js +93 -1
- package/src/render.js +19 -18
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -10,6 +10,41 @@ const path = require("path");
|
|
|
10
10
|
const { getConfig } = require("./config");
|
|
11
11
|
const { getSharedProvider } = require("./ollama");
|
|
12
12
|
const { ToolRegistry } = require("./tools");
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
|
|
15
|
+
// --- auto syntax check for AI-written code -------------------------------
|
|
16
|
+
const CHECKABLE_EXT = new Set([".py", ".js", ".mjs", ".cjs"]);
|
|
17
|
+
|
|
18
|
+
function _pick(binaries, args, timeoutMs) {
|
|
19
|
+
for (const bin of binaries) {
|
|
20
|
+
try {
|
|
21
|
+
const r = spawnSync(bin, args, { timeout: timeoutMs });
|
|
22
|
+
if (r.status === 0) return null;
|
|
23
|
+
if (r.error && r.error.code === "ENOENT") continue;
|
|
24
|
+
const err = (r.stderr ? r.stderr.toString() : "")
|
|
25
|
+
|| (r.stdout ? r.stdout.toString() : "") || `exit ${r.status}`;
|
|
26
|
+
return err.split("\n").filter((l) => l.trim()).slice(0, 4).join("\n").slice(0, 500);
|
|
27
|
+
} catch { /* try next */ }
|
|
28
|
+
}
|
|
29
|
+
return null; // no interpreter available - skip silently
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function syntaxCheckError(filePath) {
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (!CHECKABLE_EXT.has(ext)) return null;
|
|
35
|
+
if (!fs.existsSync(filePath)) return null;
|
|
36
|
+
if (ext === ".py") {
|
|
37
|
+
return _pick(["python3", "python"], ["-m", "py_compile", filePath], 15000);
|
|
38
|
+
}
|
|
39
|
+
return _pick([process.execPath || "node", "node"], ["--check", filePath], 15000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function autoCheckMsg(p, err) {
|
|
43
|
+
// Loop-break redirect style: tiny models need the exact shape again.
|
|
44
|
+
return "[auto-check] The file you just wrote FAILED its syntax check:\n" +
|
|
45
|
+
`${p}\n${err}\n` +
|
|
46
|
+
"Rewrite the ENTIRE file correctly NOW with write_file (bare JSON tool call), fixing every reported error. Do not explain.";
|
|
47
|
+
}
|
|
13
48
|
|
|
14
49
|
const TOOL_RE = /\x00TOOL:([^:]*):([^:]*):(.*?)\x00/g;
|
|
15
50
|
const USAGE_RE = /\x00USAGE:(.*?)\x00/;
|
|
@@ -74,6 +109,9 @@ const TOOL_GUIDANCE = (
|
|
|
74
109
|
"'/home/user/...' or '/Users/...' - they do not exist on this machine.\n" +
|
|
75
110
|
"- Tool arguments are REAL values: 'path': 'my_folder/app.py'. Never send the " +
|
|
76
111
|
"parameter schema ({'type': 'string'}) as a value, and never leave arguments empty.\n" +
|
|
112
|
+
"- Call tools ONLY as exact JSON: {\"name\": \"write_file\", \"arguments\": {...}}. " +
|
|
113
|
+
"NEVER invent formats like 'name_file:path' or bare '<html>' dumps - those are NOT " +
|
|
114
|
+
"tool calls, they will be ignored, and you must redo them properly.\n" +
|
|
77
115
|
"- To act, output plain JSON tool calls (never shell commands in code fences):\n" +
|
|
78
116
|
'{"name": "write_file", "arguments": {"path": "widgets/gadget.py", ' +
|
|
79
117
|
'"content": "print(\'hi\')"}}\n' +
|
|
@@ -160,6 +198,21 @@ function repliedWithoutActing(reply) {
|
|
|
160
198
|
return SLOTH_RE.test(reply) || reply.includes("```");
|
|
161
199
|
}
|
|
162
200
|
|
|
201
|
+
// Tiny models degenerate into printing the same chunk over and over
|
|
202
|
+
// ("name_file:web <html>..." x20). Detect any meaningful line repeated
|
|
203
|
+
// too many times so the agent can cut the run instead of spamming chat.
|
|
204
|
+
function degenerateRepeat(text, minLen = 12, maxHits = 4) {
|
|
205
|
+
const counts = new Map();
|
|
206
|
+
for (const rawLine of String(text || "").split("\n")) {
|
|
207
|
+
const line = rawLine.trim();
|
|
208
|
+
if (line.length < minLen) continue;
|
|
209
|
+
const n = (counts.get(line) || 0) + 1;
|
|
210
|
+
counts.set(line, n);
|
|
211
|
+
if (n >= maxHits) return line;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
163
216
|
// "Created the folder." style announcements. Harmless on their own, but a
|
|
164
217
|
// red flag when the request named files that were never actually created
|
|
165
218
|
// (e.g. 'a folder AND an index.html' - folder done, file skipped).
|
|
@@ -570,7 +623,9 @@ class Agent {
|
|
|
570
623
|
let full = "";
|
|
571
624
|
let rounds = 0;
|
|
572
625
|
let nudges = 0;
|
|
626
|
+
this._autoChecks = 0;
|
|
573
627
|
let lastSig = null;
|
|
628
|
+
let collectedText = "";
|
|
574
629
|
let loopBreaks = 0;
|
|
575
630
|
const doneBits = [];
|
|
576
631
|
|
|
@@ -633,6 +688,15 @@ class Agent {
|
|
|
633
688
|
doneBits.push(result.output.split("\n")[0].slice(0, 90));
|
|
634
689
|
const ev = codeEvent(tc, pre);
|
|
635
690
|
if (ev) full += (full.trim() ? "\n\n" : "") + codeMarkdown(ev);
|
|
691
|
+
// Auto syntax check: broken AI-written code gets one bounce-back.
|
|
692
|
+
if (ev && (tc.name === "write_file" || tc.name === "edit_file")
|
|
693
|
+
&& this._autoChecks < 2) {
|
|
694
|
+
const err = syntaxCheckError(ev.path);
|
|
695
|
+
if (err) {
|
|
696
|
+
this._autoChecks++;
|
|
697
|
+
this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
636
700
|
}
|
|
637
701
|
}
|
|
638
702
|
} else {
|
|
@@ -682,11 +746,13 @@ class Agent {
|
|
|
682
746
|
this.history.push({ role: "user", content: userInput, ts: Date.now() / 1000 });
|
|
683
747
|
let rounds = 0;
|
|
684
748
|
let nudges = 0;
|
|
749
|
+
this._autoChecks = 0;
|
|
685
750
|
const tStart = Date.now();
|
|
686
751
|
let ttft = 0;
|
|
687
752
|
let nTokens = 0;
|
|
688
753
|
this.lastRun = {};
|
|
689
754
|
let lastSig = null;
|
|
755
|
+
let collectedText = "";
|
|
690
756
|
let loopBreaks = 0;
|
|
691
757
|
const doneBits = [];
|
|
692
758
|
let sawText = false;
|
|
@@ -735,6 +801,20 @@ class Agent {
|
|
|
735
801
|
}
|
|
736
802
|
|
|
737
803
|
const full = contentParts.join("");
|
|
804
|
+
// Degeneration breaker: same meaningful line 4+ times in ONE round
|
|
805
|
+
// (or across the whole reply) -> stop instead of spamming chat.
|
|
806
|
+
const rep = degenerateRepeat(full)
|
|
807
|
+
|| (full.trim().length < 40 && degenerateRepeat(collectedText, 8, 6));
|
|
808
|
+
if (rep) {
|
|
809
|
+
this.history.push({ role: "assistant", content: full, ts: Date.now() / 1000 });
|
|
810
|
+
yield "\n· repeated output detected - cutting it off\n";
|
|
811
|
+
if (wantsAction(userInput)) {
|
|
812
|
+
this.history.push({ role: "user", content: redirectMsg(userInput), ts: Date.now() / 1000 });
|
|
813
|
+
if (loopBreaks < 2) { loopBreaks++; continue; }
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
collectedText = (collectedText ? collectedText + "\n" : "") + full;
|
|
738
818
|
this.usage.prompt += usage.prompt;
|
|
739
819
|
this.usage.completion += usage.completion;
|
|
740
820
|
this.usage.total += usage.total;
|
|
@@ -790,6 +870,16 @@ class Agent {
|
|
|
790
870
|
}
|
|
791
871
|
const ev = codeEvent(tc, pre);
|
|
792
872
|
if (ev) yield "\n" + codeMarkdown(ev) + "\n";
|
|
873
|
+
// Auto syntax check: broken AI-written code gets one bounce-back.
|
|
874
|
+
if (ev && (tc.name === "write_file" || tc.name === "edit_file")
|
|
875
|
+
&& this._autoChecks < 2) {
|
|
876
|
+
const err = syntaxCheckError(ev.path);
|
|
877
|
+
if (err) {
|
|
878
|
+
this._autoChecks++;
|
|
879
|
+
yield `\n[auto-check] ${path.basename(ev.path)} has a syntax error - fixing...\n`;
|
|
880
|
+
this.history.push({ role: "user", content: autoCheckMsg(ev.path, err), ts: Date.now() / 1000 });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
793
883
|
}
|
|
794
884
|
}
|
|
795
885
|
} else {
|
|
@@ -909,4 +999,6 @@ class Agent {
|
|
|
909
999
|
}
|
|
910
1000
|
}
|
|
911
1001
|
|
|
912
|
-
module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
|
|
1002
|
+
module.exports = { Agent, extractJsonTools, repairToolJson, wantsAction,
|
|
1003
|
+
repliedWithoutActing, stalledHalfway, syntaxCheckError, autoCheckMsg,
|
|
1004
|
+
degenerateRepeat };
|
package/src/render.js
CHANGED
|
@@ -257,30 +257,29 @@ class SmoothPrinter {
|
|
|
257
257
|
let preview = this.buf;
|
|
258
258
|
if (preview.length > MAX_PREVIEW_CHARS) preview = preview.slice(-MAX_PREVIEW_CHARS);
|
|
259
259
|
const rendered = renderMarkdown(preview, width);
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const maxRows = (this.out.rows || 2000) -
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
let lines = rendered.split("\n");
|
|
261
|
+
const target = Math.max(0, lines.length - 1);
|
|
262
|
+
const maxRows = Math.max(3, (this.out.rows || 2000) - 2);
|
|
263
|
+
// Viewport with INCREMENTAL REVEAL: grow by one row per tick so the
|
|
264
|
+
// painted block never pushes past the screen edge (which would scroll
|
|
265
|
+
// ghosts in and desync the next erase). Once at maxRows it repaints a
|
|
266
|
+
// stable window; finish() prints the complete styled message.
|
|
267
|
+
let take = Math.min(target, maxRows, this.drawn + 1);
|
|
268
|
+
if (target < take) take = target;
|
|
269
|
+
lines = lines.slice(-(take + 1));
|
|
269
270
|
this._erase();
|
|
270
|
-
this.out.write(
|
|
271
|
-
this.drawn =
|
|
271
|
+
this.out.write(lines.join("\n"));
|
|
272
|
+
this.drawn = Math.max(0, lines.length - 1);
|
|
272
273
|
this.paintWidth = cols || width;
|
|
273
274
|
this.lastPaint = now;
|
|
274
275
|
}
|
|
275
276
|
|
|
276
277
|
_degrade(skipFlush) {
|
|
277
278
|
this.degraded = true;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this.out.write("\n");
|
|
283
|
-
}
|
|
279
|
+
// Do NOT reprint the buffer here - part of it is already on screen
|
|
280
|
+
// and repainting would duplicate it. Just mark the seam and let the
|
|
281
|
+
// remaining chunks stream raw.
|
|
282
|
+
this.out.write("\n");
|
|
284
283
|
this.buf = "";
|
|
285
284
|
}
|
|
286
285
|
|
|
@@ -288,7 +287,9 @@ class SmoothPrinter {
|
|
|
288
287
|
if (this.closed) return;
|
|
289
288
|
this.closed = true;
|
|
290
289
|
if (this.timer) { clearTimeout(this.timer); this.timer = null; }
|
|
291
|
-
if (!this.tty
|
|
290
|
+
if (!this.tty) return;
|
|
291
|
+
// Always un-hide the cursor, degraded or not.
|
|
292
|
+
if (this.degraded) { this.out.write(`${ESC}[?25h`); return; }
|
|
292
293
|
this._erase();
|
|
293
294
|
const width = this.widthOf();
|
|
294
295
|
this.out.write(renderMarkdown(this.buf, width) + "\n");
|