@ducci/jarvis 1.0.65 → 1.0.67
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/docs/system-prompt.md +1 -1
- package/package.json +1 -1
- package/src/server/agent.js +41 -6
package/docs/system-prompt.md
CHANGED
|
@@ -55,7 +55,7 @@ There are two types of responses depending on whether you need to use tools:
|
|
|
55
55
|
"logSummary": "A concise explanation of what you did and why, written for a human reading the logs."
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
The `response` value must be a string — never an array or object. Use HTML formatting tags for readability — only these Telegram-supported tags are allowed: <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strikethrough</s>, <code>inline code</code>, <pre>code block</pre>, <blockquote>quote</blockquote>, <a href="URL">link</a>. For line breaks use actual newlines (\n), never <br>. Never use Markdown formatting (no **, __, `, or ```). If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
58
|
+
The `response` value must be a string — never an array or object. Use HTML formatting tags for readability — only these Telegram-supported tags are allowed: <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strikethrough</s>, <code>inline code</code>, <pre>code block</pre>, <blockquote>quote</blockquote>, <a href="URL">link</a>. For line breaks use actual newlines (\n), never <br>. Never use Markdown formatting (no **, __, `, or ```). Always escape literal `<`, `>`, and `&` characters as `<`, `>`, and `&` — this applies everywhere including inside `<code>` and `<pre>` blocks (e.g. HTML snippets, shell redirects, comparisons like `x < 5`, generics like `List<String>`). In `<a href="">` URLs, escape `&` in query parameters as `&` (e.g. `?foo=1&bar=2`). Unescaped characters cause Telegram to reject the message entirely. If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
59
59
|
|
|
60
60
|
Never include markdown code fences, preamble, or any text outside this JSON object. If you cannot complete a task, explain why in the `response` field — still as valid JSON.
|
|
61
61
|
|
package/package.json
CHANGED
package/src/server/agent.js
CHANGED
|
@@ -15,6 +15,41 @@ const LOOP_DETECTION_THRESHOLD = 3;
|
|
|
15
15
|
function stripCodeFence(text) {
|
|
16
16
|
return text.replace(/^```(?:json)?\s*\n([\s\S]*?)\n?```\s*$/, '$1').trim();
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
// Sanitize raw model output before JSON.parse:
|
|
20
|
+
// 1. Strip code fences
|
|
21
|
+
// 2. Escape literal control characters (e.g. real newlines in <pre> blocks) inside
|
|
22
|
+
// JSON string values — models sometimes forget to escape \n as \\n
|
|
23
|
+
// 3. Remove trailing commas before } and ] (JS-valid but JSON-invalid)
|
|
24
|
+
function sanitizeJson(text) {
|
|
25
|
+
let s = stripCodeFence(text);
|
|
26
|
+
|
|
27
|
+
// State machine: walk the string and fix unescaped control chars inside strings
|
|
28
|
+
let result = '';
|
|
29
|
+
let inString = false;
|
|
30
|
+
let escaped = false;
|
|
31
|
+
const controlEscapes = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
|
|
32
|
+
for (let i = 0; i < s.length; i++) {
|
|
33
|
+
const ch = s[i];
|
|
34
|
+
if (escaped) {
|
|
35
|
+
result += ch;
|
|
36
|
+
escaped = false;
|
|
37
|
+
} else if (ch === '\\' && inString) {
|
|
38
|
+
result += ch;
|
|
39
|
+
escaped = true;
|
|
40
|
+
} else if (ch === '"') {
|
|
41
|
+
result += ch;
|
|
42
|
+
inString = !inString;
|
|
43
|
+
} else if (inString && ch.charCodeAt(0) < 0x20) {
|
|
44
|
+
result += controlEscapes[ch] ?? `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
45
|
+
} else {
|
|
46
|
+
result += ch;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Remove trailing commas before } and ]
|
|
51
|
+
return result.replace(/,(\s*[}\]])/g, '$1');
|
|
52
|
+
}
|
|
18
53
|
const CONSECUTIVE_FAILURE_THRESHOLD = 3;
|
|
19
54
|
const MAX_TOOL_RESULT = 4000;
|
|
20
55
|
|
|
@@ -402,20 +437,20 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
402
437
|
if (nudgeContent.trim()) {
|
|
403
438
|
content = nudgeContent;
|
|
404
439
|
}
|
|
405
|
-
parsed = JSON.parse(
|
|
440
|
+
parsed = JSON.parse(sanitizeJson(nudgeContent));
|
|
406
441
|
} catch {
|
|
407
442
|
// Fall through to !parsed handler; content may now carry the nudge text
|
|
408
443
|
}
|
|
409
444
|
} else {
|
|
410
445
|
try {
|
|
411
|
-
parsed = JSON.parse(
|
|
446
|
+
parsed = JSON.parse(sanitizeJson(content));
|
|
412
447
|
} catch {
|
|
413
448
|
// Step 1: retry with fallback model
|
|
414
449
|
try {
|
|
415
450
|
const fallbackResult = await callModel(client, config.fallbackModel, preparedMessages, toolDefs);
|
|
416
451
|
accumulateUsage(usageAccum, fallbackResult);
|
|
417
452
|
const fallbackContent = fallbackResult.choices[0]?.message?.content || '';
|
|
418
|
-
parsed = JSON.parse(
|
|
453
|
+
parsed = JSON.parse(sanitizeJson(fallbackContent));
|
|
419
454
|
content = fallbackContent;
|
|
420
455
|
} catch {
|
|
421
456
|
// Step 2: nudge retry via both models
|
|
@@ -424,7 +459,7 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
424
459
|
const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, toolDefs);
|
|
425
460
|
accumulateUsage(usageAccum, nudgeResult);
|
|
426
461
|
const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
|
|
427
|
-
parsed = JSON.parse(
|
|
462
|
+
parsed = JSON.parse(sanitizeJson(nudgeContent));
|
|
428
463
|
content = nudgeContent;
|
|
429
464
|
} catch {
|
|
430
465
|
// Give up
|
|
@@ -495,14 +530,14 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
495
530
|
|
|
496
531
|
// Try JSON parse; if it fails, nudge retry (Layer 2)
|
|
497
532
|
try {
|
|
498
|
-
parsedWrapUp = JSON.parse(
|
|
533
|
+
parsedWrapUp = JSON.parse(sanitizeJson(wrapUpContent));
|
|
499
534
|
} catch {
|
|
500
535
|
try {
|
|
501
536
|
const nudgeMessages = [...wrapUpMessages, { role: 'user', content: FORMAT_NUDGE }];
|
|
502
537
|
const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, []);
|
|
503
538
|
accumulateUsage(usageAccum, nudgeResult);
|
|
504
539
|
const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
|
|
505
|
-
parsedWrapUp = JSON.parse(
|
|
540
|
+
parsedWrapUp = JSON.parse(sanitizeJson(nudgeContent));
|
|
506
541
|
wrapUpContent = nudgeContent;
|
|
507
542
|
} catch {
|
|
508
543
|
// Layer 3: use raw text as best-effort response below
|