@ducci/jarvis 1.0.10 → 1.0.11

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/agent.md CHANGED
@@ -198,7 +198,7 @@ Seed tool included for sanity checks:
198
198
  Jarvis uses the provider tool-calling API:
199
199
 
200
200
  1. The model returns an assistant message containing a `tool_calls` array.
201
- 2. Jarvis appends that assistant message to the conversation history as-is.
201
+ 2. Jarvis normalizes each tool call before appending to the conversation history: if `function.arguments` is missing or empty, it is set to `"{}"`. Some models (especially smaller/free ones) omit `arguments` for no-arg tools. Storing a malformed tool call would cause the next API request to fail with a 400 validation error.
202
202
  3. Jarvis executes those tools in order, serially.
203
203
  4. Each tool result is appended to the conversation as a `role: "tool"` message with a matching `tool_call_id`.
204
204
  5. Jarvis calls the model again with the updated conversation.
@@ -447,7 +447,13 @@ Tool inputs/outputs:
447
447
 
448
448
  - Model call failures: try the selected model once, then one fallback model attempt. If both fail, end the run with a `500` error and a clear message.
449
449
  - Tool failures: pass the error result back to the model and continue the loop. Best case would be that the next model response include another tool call to fix the previous tool call. All tool errors (especially `exec` failures) must be reported in the `logSummary` with enough detail for a human to understand the cause.
450
- - Malformed JSON on final response: log the failure and stop the run with a formatted error message.
450
+ - Malformed JSON on final response: attempt two recovery steps before giving up:
451
+ 1. **Fallback model retry** — call the fallback model with the same conversation messages (the bad response is not saved to the session yet). If this produces valid JSON, use it and continue normally.
452
+ 2. **Nudge retry** — if the fallback model also returns non-JSON, append a temporary nudge message to the conversation (not saved to the session) and call `callModelWithFallback` once more:
453
+ ```
454
+ Your previous response was not valid JSON. Respond only with the required JSON object: {"response": "...", "logSummary": "..."}
455
+ ```
456
+ 3. **Give up** — if all three attempts fail, return `format_error` without pushing any assistant content to the session. The nudge message is never persisted regardless of outcome.
451
457
 
452
458
  **Error Payload Structure**:
453
459
 
@@ -462,6 +468,14 @@ Tool inputs/outputs:
462
468
  - Use `500 Internal Server Error` for API failures, tool runtime errors, or model communication issues.
463
469
  - Always append a log entry on failure so the outcome is visible in the session log.
464
470
 
471
+ **Synthetic error note on failure**: when a run ends with `model_error` or `format_error`, a synthetic assistant message is appended to the session before saving:
472
+
473
+ ```
474
+ [System: Previous run failed (model_error): <logSummary>. Error detail: <errorDetail JSON>]
475
+ ```
476
+
477
+ The full `errorDetail` (provider error body, HTTP status, etc.) is included so the model has enough information to understand and potentially recover from the failure without needing to call `read_session_log`. Without this, the session would contain a dangling user message with no reply, and the model would have no way to understand or recover from the failure.
478
+
465
479
  Model configuration:
466
480
 
467
481
  - Selected model ID is stored in the same config file created during setup.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -6,6 +6,8 @@ import { loadTools, getToolDefinitions, executeTool } from './tools.js';
6
6
  import { appendLog } from './logging.js';
7
7
  import chalk from 'chalk';
8
8
 
9
+ const FORMAT_NUDGE = 'Your previous response was not valid JSON. Respond only with the required JSON object: {"response": "...", "logSummary": "..."}';
10
+
9
11
  const WRAP_UP_NOTE = `[System: You have reached the iteration limit. This is your final response for this run.
10
12
  Respond with your normal JSON, but add a checkpoint field:
11
13
 
@@ -112,7 +114,13 @@ async function runAgentLoop(client, config, session, prepareMessages) {
112
114
  session.messages.push({
113
115
  role: 'assistant',
114
116
  content: assistantMessage.content || null,
115
- tool_calls: assistantMessage.tool_calls,
117
+ tool_calls: assistantMessage.tool_calls.map(tc => ({
118
+ ...tc,
119
+ function: {
120
+ ...tc.function,
121
+ arguments: tc.function.arguments || '{}',
122
+ },
123
+ })),
116
124
  });
117
125
 
118
126
  let toolsModified = false;
@@ -158,20 +166,45 @@ async function runAgentLoop(client, config, session, prepareMessages) {
158
166
  }
159
167
 
160
168
  // No tool calls — final response
161
- const content = assistantMessage.content || '';
162
- session.messages.push({ role: 'assistant', content });
169
+ // Delay pushing to session until we have a valid response (recovery may replace it)
170
+ let content = assistantMessage.content || '';
171
+ let parsed = null;
163
172
 
164
173
  try {
165
- const parsed = JSON.parse(content);
166
- response = parsed.response || content;
167
- logSummary = parsed.logSummary || '';
174
+ parsed = JSON.parse(content);
168
175
  } catch {
176
+ // Step 1: retry with fallback model
177
+ try {
178
+ const fallbackResult = await callModel(client, config.fallbackModel, preparedMessages, toolDefs);
179
+ const fallbackContent = fallbackResult.choices[0]?.message?.content || '';
180
+ parsed = JSON.parse(fallbackContent);
181
+ content = fallbackContent;
182
+ } catch {
183
+ // Step 2: nudge retry via both models
184
+ try {
185
+ const nudgeMessages = [...preparedMessages, { role: 'user', content: FORMAT_NUDGE }];
186
+ const nudgeResult = await callModelWithFallback(client, config, nudgeMessages, toolDefs);
187
+ const nudgeContent = nudgeResult.choices[0]?.message?.content || '';
188
+ parsed = JSON.parse(nudgeContent);
189
+ content = nudgeContent;
190
+ } catch {
191
+ // Give up
192
+ }
193
+ }
194
+ }
195
+
196
+ if (!parsed) {
197
+ // Don't push bad content — handleChat will inject a synthetic error note
169
198
  response = content;
170
- logSummary = 'Model returned non-JSON final response.';
199
+ logSummary = 'Model returned non-JSON final response after recovery attempts.';
171
200
  status = 'format_error';
172
201
  return { iteration, response, logSummary, status, runToolCalls, checkpoint: null, rawResponse: content };
173
202
  }
174
203
 
204
+ session.messages.push({ role: 'assistant', content });
205
+ response = parsed.response || content;
206
+ logSummary = parsed.logSummary || '';
207
+
175
208
  done = true;
176
209
  break;
177
210
  }
@@ -303,6 +336,16 @@ export async function handleChat(config, requestSessionId, userMessage) {
303
336
  if (run.contextInfo) logEntry.contextInfo = run.contextInfo;
304
337
  if (run.rawResponse) logEntry.rawResponse = run.rawResponse;
305
338
  appendLog(sessionId, logEntry);
339
+
340
+ // Inject synthetic error note so the model has context on the next user turn
341
+ if (finalStatus === 'model_error' || finalStatus === 'format_error') {
342
+ const errorDetail = run.errorDetail ? ` Error detail: ${JSON.stringify(run.errorDetail)}` : '';
343
+ session.messages.push({
344
+ role: 'assistant',
345
+ content: `[System: Previous run failed (${finalStatus}): ${finalLogSummary}.${errorDetail}]`,
346
+ });
347
+ }
348
+
306
349
  break;
307
350
  }
308
351