@ducci/jarvis 1.0.11 → 1.0.13

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.
@@ -2,13 +2,17 @@
2
2
 
3
3
  This is the authoritative system prompt sent to the model at the start of every session. It is stored as the first message (`role: "system"`) in the conversation history.
4
4
 
5
- Before sending to the model, the server replaces the `{{user_info}}` placeholder with the current contents of `user-info.json`. This happens at runtime on every request — the placeholder is never stored in the conversation history.
5
+ Before sending to the model, the server replaces the `{{user_info}}` and `{{session_id}}` placeholders at runtime on every request — these are never stored in the conversation history.
6
6
 
7
7
  ---
8
8
 
9
9
  ```
10
10
  You are Jarvis, a fully autonomous agent running on a local server. You have access to tools and can execute shell commands on the machine you run on.
11
11
 
12
+ ## Session
13
+
14
+ Current session ID: {{session_id}}
15
+
12
16
  ## Known User Context
13
17
 
14
18
  {{user_info}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import { appendLog } from './logging.js';
7
7
  import chalk from 'chalk';
8
8
 
9
9
  const FORMAT_NUDGE = 'Your previous response was not valid JSON. Respond only with the required JSON object: {"response": "...", "logSummary": "..."}';
10
+ const LOOP_DETECTION_THRESHOLD = 3;
10
11
 
11
12
  const WRAP_UP_NOTE = `[System: You have reached the iteration limit. This is your final response for this run.
12
13
  Respond with your normal JSON, but add a checkpoint field:
@@ -69,6 +70,7 @@ async function runAgentLoop(client, config, session, prepareMessages) {
69
70
  let toolDefs = getToolDefinitions(tools);
70
71
  let iteration = 0;
71
72
  const runToolCalls = [];
73
+ const loopTracker = new Map();
72
74
  let done = false;
73
75
  let response = '';
74
76
  let logSummary = '';
@@ -154,6 +156,17 @@ async function runAgentLoop(client, config, session, prepareMessages) {
154
156
  tool_call_id: toolCall.id,
155
157
  content: resultStr,
156
158
  });
159
+
160
+ const callKey = `${toolName}|${JSON.stringify(toolArgs)}|${resultStr}`;
161
+ loopTracker.set(callKey, (loopTracker.get(callKey) || 0) + 1);
162
+ }
163
+
164
+ const loopDetected = [...loopTracker.values()].some(count => count >= LOOP_DETECTION_THRESHOLD);
165
+ if (loopDetected) {
166
+ session.messages.push({
167
+ role: 'user',
168
+ content: '[System: Loop detected. You are repeatedly calling the same tools with identical arguments and getting identical results. Stop calling tools and provide your final answer now based on what you already know.]',
169
+ });
157
170
  }
158
171
 
159
172
  // Reload tools if any were created/updated this iteration
@@ -245,31 +258,47 @@ async function runAgentLoop(client, config, session, prepareMessages) {
245
258
  };
246
259
  }
247
260
 
248
- const wrapUpContent = wrapUpResult.choices[0].message.content || '';
249
- // Store the wrap-up response (but NOT the temporary system note)
250
- session.messages.push({ role: 'assistant', content: wrapUpContent });
261
+ let wrapUpContent = wrapUpResult.choices[0].message.content || '';
262
+ let parsedWrapUp = null;
251
263
 
264
+ // Try JSON parse; if it fails, nudge retry (Layer 2)
252
265
  try {
253
- const parsed = JSON.parse(wrapUpContent);
254
- response = parsed.response || '';
255
- logSummary = parsed.logSummary || '';
266
+ parsedWrapUp = JSON.parse(wrapUpContent);
267
+ } catch {
268
+ try {
269
+ const nudgeMessages = [...wrapUpMessages, { role: 'user', content: FORMAT_NUDGE }];
270
+ const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, []);
271
+ const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
272
+ parsedWrapUp = JSON.parse(nudgeContent);
273
+ wrapUpContent = nudgeContent;
274
+ } catch {
275
+ // Layer 3: use raw text as best-effort response below
276
+ }
277
+ }
278
+
279
+ // Store the wrap-up response (but NOT the temporary system note)
280
+ session.messages.push({ role: 'assistant', content: wrapUpContent });
256
281
 
257
- if (parsed.checkpoint) {
282
+ if (parsedWrapUp) {
283
+ response = parsedWrapUp.response || '';
284
+ logSummary = parsedWrapUp.logSummary || '';
285
+ if (parsedWrapUp.checkpoint) {
258
286
  return {
259
287
  iteration,
260
288
  response,
261
289
  logSummary,
262
290
  status: 'checkpoint_reached',
263
291
  runToolCalls,
264
- checkpoint: parsed.checkpoint,
292
+ checkpoint: parsedWrapUp.checkpoint,
265
293
  };
266
294
  }
267
- } catch {
295
+ status = 'ok';
296
+ } else {
297
+ // Layer 3: use raw text — user gets a real response instead of an error
268
298
  response = wrapUpContent;
269
- logSummary = 'Wrap-up response was not valid JSON.';
299
+ logSummary = 'Wrap-up response was not valid JSON after retry.';
300
+ status = 'ok';
270
301
  }
271
-
272
- status = 'checkpoint_reached';
273
302
  }
274
303
 
275
304
  return { iteration, response, logSummary, status, runToolCalls, checkpoint: null };
@@ -301,7 +330,7 @@ export async function handleChat(config, requestSessionId, userMessage) {
301
330
  function prepareMessages(messages) {
302
331
  return messages.map((msg, i) => {
303
332
  if (i === 0 && msg.role === 'system') {
304
- return { ...msg, content: resolveSystemPrompt(msg.content) };
333
+ return { ...msg, content: resolveSystemPrompt(msg.content, sessionId) };
305
334
  }
306
335
  return msg;
307
336
  });
@@ -63,7 +63,7 @@ export function loadSystemPrompt() {
63
63
  return match[1].trim();
64
64
  }
65
65
 
66
- export function resolveSystemPrompt(promptTemplate) {
66
+ export function resolveSystemPrompt(promptTemplate, sessionId) {
67
67
  let userInfo = '(none yet)';
68
68
  try {
69
69
  const raw = fs.readFileSync(PATHS.userInfoFile, 'utf8');
@@ -74,5 +74,7 @@ export function resolveSystemPrompt(promptTemplate) {
74
74
  } catch {
75
75
  // File doesn't exist yet
76
76
  }
77
- return promptTemplate.replace('{{user_info}}', userInfo);
77
+ return promptTemplate
78
+ .replace('{{session_id}}', sessionId || 'unknown')
79
+ .replace('{{user_info}}', userInfo);
78
80
  }