@ducci/jarvis 1.0.51 → 1.0.53

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": "@ducci/jarvis",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -9,6 +9,12 @@ import chalk from 'chalk';
9
9
 
10
10
  const FORMAT_NUDGE = 'Your previous response was not valid JSON. Respond only with the required JSON object: {"response": "...", "logSummary": "..."}';
11
11
  const LOOP_DETECTION_THRESHOLD = 3;
12
+
13
+ // Strip markdown code fences (```json...``` or ```...```) that models sometimes
14
+ // wrap around JSON responses, which would otherwise cause JSON.parse to throw.
15
+ function stripCodeFence(text) {
16
+ return text.replace(/^```(?:json)?\s*\n([\s\S]*?)\n?```\s*$/, '$1').trim();
17
+ }
12
18
  const CONSECUTIVE_FAILURE_THRESHOLD = 3;
13
19
  const MAX_TOOL_RESULT = 4000;
14
20
 
@@ -396,20 +402,20 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
396
402
  if (nudgeContent.trim()) {
397
403
  content = nudgeContent;
398
404
  }
399
- parsed = JSON.parse(nudgeContent);
405
+ parsed = JSON.parse(stripCodeFence(nudgeContent));
400
406
  } catch {
401
407
  // Fall through to !parsed handler; content may now carry the nudge text
402
408
  }
403
409
  } else {
404
410
  try {
405
- parsed = JSON.parse(content);
411
+ parsed = JSON.parse(stripCodeFence(content));
406
412
  } catch {
407
413
  // Step 1: retry with fallback model
408
414
  try {
409
415
  const fallbackResult = await callModel(client, config.fallbackModel, preparedMessages, toolDefs);
410
416
  accumulateUsage(usageAccum, fallbackResult);
411
417
  const fallbackContent = fallbackResult.choices[0]?.message?.content || '';
412
- parsed = JSON.parse(fallbackContent);
418
+ parsed = JSON.parse(stripCodeFence(fallbackContent));
413
419
  content = fallbackContent;
414
420
  } catch {
415
421
  // Step 2: nudge retry via both models
@@ -418,7 +424,7 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
418
424
  const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, toolDefs);
419
425
  accumulateUsage(usageAccum, nudgeResult);
420
426
  const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
421
- parsed = JSON.parse(nudgeContent);
427
+ parsed = JSON.parse(stripCodeFence(nudgeContent));
422
428
  content = nudgeContent;
423
429
  } catch {
424
430
  // Give up
@@ -489,14 +495,14 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
489
495
 
490
496
  // Try JSON parse; if it fails, nudge retry (Layer 2)
491
497
  try {
492
- parsedWrapUp = JSON.parse(wrapUpContent);
498
+ parsedWrapUp = JSON.parse(stripCodeFence(wrapUpContent));
493
499
  } catch {
494
500
  try {
495
501
  const nudgeMessages = [...wrapUpMessages, { role: 'user', content: FORMAT_NUDGE }];
496
502
  const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, []);
497
503
  accumulateUsage(usageAccum, nudgeResult);
498
504
  const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
499
- parsedWrapUp = JSON.parse(nudgeContent);
505
+ parsedWrapUp = JSON.parse(stripCodeFence(nudgeContent));
500
506
  wrapUpContent = nudgeContent;
501
507
  } catch {
502
508
  // Layer 3: use raw text as best-effort response below
@@ -60,8 +60,61 @@ export async function runCron(entry, config) {
60
60
  }
61
61
 
62
62
  let run;
63
+ let handoffCount = 0;
64
+ let previousRemaining = null;
65
+ const failedApproaches = [];
66
+ const checkpointState = {};
67
+
63
68
  try {
64
- run = await runAgentLoop(client, config, session, prepareMessages, usageAccum);
69
+ while (true) {
70
+ const runStartIndex = session.messages.length;
71
+
72
+ try {
73
+ run = await runAgentLoop(client, config, session, prepareMessages, usageAccum);
74
+ } catch (e) {
75
+ run = { status: 'error', response: e.message, logSummary: e.message, runToolCalls: [] };
76
+ break;
77
+ }
78
+
79
+ if (run.status !== 'checkpoint_reached') break;
80
+
81
+ if (run.checkpoint.failedApproaches?.length > 0) {
82
+ failedApproaches.push(...run.checkpoint.failedApproaches);
83
+ }
84
+ if (run.checkpoint.state && Object.keys(run.checkpoint.state).length > 0) {
85
+ Object.assign(checkpointState, run.checkpoint.state);
86
+ }
87
+
88
+ // Zero-progress detection
89
+ const currentRemaining = (run.checkpoint.remaining || '').trim();
90
+ if (previousRemaining !== null && currentRemaining === previousRemaining) {
91
+ run = { ...run, status: 'intervention_required', logSummary: 'Zero progress detected in cron run.' };
92
+ session.messages.splice(runStartIndex, session.messages.length - runStartIndex - 1);
93
+ break;
94
+ }
95
+ previousRemaining = currentRemaining;
96
+
97
+ // Max handoffs
98
+ handoffCount++;
99
+ if (handoffCount > config.maxHandoffs) {
100
+ run = { ...run, status: 'intervention_required', logSummary: 'Max handoffs exceeded in cron run.' };
101
+ session.messages.splice(runStartIndex, session.messages.length - runStartIndex - 1);
102
+ break;
103
+ }
104
+
105
+ // Strip intermediate tool history, keep wrap-up assistant response
106
+ session.messages.splice(runStartIndex, session.messages.length - runStartIndex - 1);
107
+
108
+ // Resume with checkpoint.remaining + accumulated context
109
+ let resumeContent = run.checkpoint.remaining || 'Continue with the task.';
110
+ if (failedApproaches.length > 0) {
111
+ resumeContent += `\n\n[System: The following approaches were tried and failed in previous runs — do not repeat them:\n${failedApproaches.map((a, i) => `${i + 1}. ${a}`).join('\n')}]`;
112
+ }
113
+ if (Object.keys(checkpointState).length > 0) {
114
+ resumeContent += `\n\n[System: Known facts from previous runs:\n${Object.entries(checkpointState).map(([k, v]) => `- ${k}: ${v}`).join('\n')}]`;
115
+ }
116
+ session.messages.push({ role: 'user', content: resumeContent });
117
+ }
65
118
  } catch (e) {
66
119
  run = { status: 'error', response: e.message, logSummary: e.message, runToolCalls: [] };
67
120
  }