@ducci/jarvis 1.0.7 → 1.0.9

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.7",
3
+ "version": "1.0.9",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "setup": "node ./src/scripts/onboarding.js",
21
21
  "dev": "nodemon ./src/server/app.js",
22
22
  "prepare": "cd ui && npm install && npm run build",
23
- "publish": "node scripts/publish.js",
23
+ "release": "node scripts/publish.js",
24
24
  "test": "echo \"Error: no test specified\" && exit 1"
25
25
  },
26
26
  "keywords": [
@@ -4,6 +4,7 @@ import { loadSystemPrompt, resolveSystemPrompt } from './config.js';
4
4
  import { loadSession, saveSession, createSession } from './sessions.js';
5
5
  import { loadTools, getToolDefinitions, executeTool } from './tools.js';
6
6
  import { appendLog } from './logging.js';
7
+ import chalk from 'chalk';
7
8
 
8
9
  const WRAP_UP_NOTE = `[System: You have reached the iteration limit. This is your final response for this run.
9
10
  Respond with your normal JSON, but add a checkpoint field:
@@ -27,17 +28,33 @@ async function callModel(client, model, messages, tools) {
27
28
  return await client.chat.completions.create(params);
28
29
  }
29
30
 
31
+ function extractApiError(err, model) {
32
+ return {
33
+ model,
34
+ httpStatus: err?.status ?? null,
35
+ message: err?.message ?? String(err),
36
+ body: err?.error ?? null,
37
+ };
38
+ }
39
+
30
40
  async function callModelWithFallback(client, config, messages, tools) {
41
+ let primaryErr = null;
31
42
  try {
32
43
  return await callModel(client, config.selectedModel, messages, tools);
33
- } catch (primaryErr) {
34
- try {
35
- return await callModel(client, config.fallbackModel, messages, tools);
36
- } catch (fallbackErr) {
37
- throw new Error(
38
- `Both primary (${config.selectedModel}) and fallback (${config.fallbackModel}) models failed. Last error: ${fallbackErr.message}`
39
- );
40
- }
44
+ } catch (err) {
45
+ primaryErr = err;
46
+ }
47
+ try {
48
+ return await callModel(client, config.fallbackModel, messages, tools);
49
+ } catch (fallbackErr) {
50
+ const combined = new Error(
51
+ `Both primary (${config.selectedModel}) and fallback (${config.fallbackModel}) models failed. Last error: ${fallbackErr.message}`
52
+ );
53
+ combined.apiErrors = {
54
+ primary: extractApiError(primaryErr, config.selectedModel),
55
+ fallback: extractApiError(fallbackErr, config.fallbackModel),
56
+ };
57
+ throw combined;
41
58
  }
42
59
  }
43
60
 
@@ -57,8 +74,9 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
57
74
  iteration++;
58
75
 
59
76
  let modelResult;
77
+ const preparedMessages = prepareMessages(session.messages);
60
78
  try {
61
- modelResult = await callModelWithFallback(client, config, prepareMessages(session.messages), toolDefs);
79
+ modelResult = await callModelWithFallback(client, config, preparedMessages, toolDefs);
62
80
  } catch (e) {
63
81
  return {
64
82
  iteration,
@@ -67,6 +85,21 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
67
85
  status: 'model_error',
68
86
  runToolCalls,
69
87
  checkpoint: null,
88
+ errorDetail: e.apiErrors ?? { message: e.message, stack: e.stack },
89
+ contextInfo: { messageCount: preparedMessages.length },
90
+ };
91
+ }
92
+
93
+ if (!modelResult.choices || modelResult.choices.length === 0) {
94
+ return {
95
+ iteration,
96
+ response: 'Model returned an empty response.',
97
+ logSummary: `Model error on iteration ${iteration}: Empty choices array.`,
98
+ status: 'model_error',
99
+ runToolCalls,
100
+ checkpoint: null,
101
+ errorDetail: { message: 'Empty choices array from LLM' },
102
+ contextInfo: { messageCount: preparedMessages.length },
70
103
  };
71
104
  }
72
105
 
@@ -123,6 +156,7 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
123
156
  response = content;
124
157
  logSummary = 'Model returned non-JSON final response.';
125
158
  status = 'format_error';
159
+ return { iteration, response, logSummary, status, runToolCalls, checkpoint: null, rawResponse: content };
126
160
  }
127
161
 
128
162
  done = true;
@@ -147,6 +181,21 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
147
181
  status: 'model_error',
148
182
  runToolCalls,
149
183
  checkpoint: null,
184
+ errorDetail: e.apiErrors ?? { message: e.message, stack: e.stack },
185
+ contextInfo: { messageCount: wrapUpMessages.length },
186
+ };
187
+ }
188
+
189
+ if (!wrapUpResult.choices || wrapUpResult.choices.length === 0) {
190
+ return {
191
+ iteration,
192
+ response: 'Wrap-up call returned an empty response.',
193
+ logSummary: 'Iteration limit reached. Wrap-up returned empty choices.',
194
+ status: 'model_error',
195
+ runToolCalls,
196
+ checkpoint: null,
197
+ errorDetail: { message: 'Empty choices array in wrap-up' },
198
+ contextInfo: { messageCount: wrapUpMessages.length },
150
199
  };
151
200
  }
152
201
 
@@ -221,63 +270,85 @@ export async function handleChat(config, requestSessionId, userMessage) {
221
270
  let finalStatus = 'ok';
222
271
 
223
272
  // Handoff loop
224
- while (true) {
225
- const run = await runAgentLoop(client, config, session, tools, toolDefs, prepareMessages);
226
- allToolCalls.push(...run.runToolCalls);
227
-
228
- if (run.status !== 'checkpoint_reached') {
229
- finalResponse = run.response;
230
- finalLogSummary = run.logSummary;
231
- finalStatus = run.status;
273
+ try {
274
+ while (true) {
275
+ const run = await runAgentLoop(client, config, session, tools, toolDefs, prepareMessages);
276
+ allToolCalls.push(...run.runToolCalls);
277
+
278
+ if (run.status !== 'checkpoint_reached') {
279
+ finalResponse = run.response;
280
+ finalLogSummary = run.logSummary;
281
+ finalStatus = run.status;
282
+
283
+ const logEntry = {
284
+ iteration: run.iteration,
285
+ model: config.selectedModel,
286
+ userInput: userMessage,
287
+ toolCalls: allToolCalls,
288
+ response: finalResponse,
289
+ logSummary: finalLogSummary,
290
+ status: finalStatus,
291
+ };
292
+ if (run.errorDetail) logEntry.errorDetail = run.errorDetail;
293
+ if (run.contextInfo) logEntry.contextInfo = run.contextInfo;
294
+ if (run.rawResponse) logEntry.rawResponse = run.rawResponse;
295
+ appendLog(sessionId, logEntry);
296
+ break;
297
+ }
232
298
 
299
+ // Checkpoint reached — log this run
233
300
  appendLog(sessionId, {
234
301
  iteration: run.iteration,
235
302
  model: config.selectedModel,
236
303
  userInput: userMessage,
237
- toolCalls: allToolCalls,
238
- response: finalResponse,
239
- logSummary: finalLogSummary,
240
- status: finalStatus,
304
+ toolCalls: run.runToolCalls,
305
+ response: run.response,
306
+ logSummary: run.logSummary,
307
+ status: 'checkpoint_reached',
241
308
  });
242
- break;
243
- }
244
-
245
- // Checkpoint reached — log this run
246
- appendLog(sessionId, {
247
- iteration: run.iteration,
248
- model: config.selectedModel,
249
- userInput: userMessage,
250
- toolCalls: run.runToolCalls,
251
- response: run.response,
252
- logSummary: run.logSummary,
253
- status: 'checkpoint_reached',
254
- });
255
309
 
256
- // Check handoff limit
257
- session.metadata.handoffCount++;
258
- if (session.metadata.handoffCount > config.maxHandoffs) {
259
- finalResponse = run.response;
260
- finalLogSummary = run.logSummary;
261
- finalStatus = 'intervention_required';
310
+ // Check handoff limit
311
+ session.metadata.handoffCount++;
312
+ if (session.metadata.handoffCount > config.maxHandoffs) {
313
+ finalResponse = run.response;
314
+ finalLogSummary = run.logSummary;
315
+ finalStatus = 'intervention_required';
316
+
317
+ appendLog(sessionId, {
318
+ iteration: 0,
319
+ model: config.selectedModel,
320
+ userInput: userMessage,
321
+ toolCalls: [],
322
+ response: finalResponse,
323
+ logSummary: 'Max handoffs exceeded. Human intervention required.',
324
+ status: 'intervention_required',
325
+ });
326
+ break;
327
+ }
262
328
 
263
- appendLog(sessionId, {
264
- iteration: 0,
265
- model: config.selectedModel,
266
- userInput: userMessage,
267
- toolCalls: [],
268
- response: finalResponse,
269
- logSummary: 'Max handoffs exceeded. Human intervention required.',
270
- status: 'intervention_required',
271
- });
272
- break;
329
+ // Resume with checkpoint.remaining as new prompt
330
+ session.messages.push({ role: 'user', content: run.checkpoint.remaining });
273
331
  }
274
-
275
- // Resume with checkpoint.remaining as new prompt
276
- session.messages.push({ role: 'user', content: run.checkpoint.remaining });
332
+ } catch (e) {
333
+ const errorLog = {
334
+ iteration: 0,
335
+ model: config.selectedModel,
336
+ userInput: userMessage,
337
+ toolCalls: allToolCalls,
338
+ response: `An unexpected server error occurred: ${e.message}`,
339
+ logSummary: `Critical error: ${e.message}`,
340
+ status: 'error',
341
+ errorDetail: { message: e.message, stack: e.stack },
342
+ };
343
+ appendLog(sessionId, errorLog);
344
+ // Re-throw to let app.js handle the HTTP response
345
+ throw e;
277
346
  }
278
347
 
279
348
  saveSession(sessionId, session);
280
349
 
350
+ console.log(`${chalk.magenta('<<<')} ${chalk.bold('Final Response')} [SID: ${chalk.dim(sessionId.slice(0, 8))}] ${chalk.italic(finalLogSummary)}`);
351
+
281
352
  return {
282
353
  sessionId,
283
354
  response: finalResponse,
package/src/server/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import express from 'express';
2
2
  import path from 'path';
3
+ import chalk from 'chalk';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { realpathSync } from 'fs';
5
6
  import { loadConfig, ensureDirectories } from './config.js';
@@ -12,6 +13,15 @@ const __dirname = path.dirname(__filename);
12
13
  const app = express();
13
14
  app.use(express.json());
14
15
 
16
+ // Request logger
17
+ app.use((req, res, next) => {
18
+ if (req.path === '/api/chat' && req.method === 'POST') {
19
+ const sid = req.body.sessionId || 'new';
20
+ console.log(`\n${chalk.magenta('>>>')} ${chalk.bold('Incoming Chat')} [SID: ${chalk.dim(sid.slice(0, 8))}]`);
21
+ }
22
+ next();
23
+ });
24
+
15
25
  // Serve the built UI as static files
16
26
  const uiDist = path.join(__dirname, '..', '..', 'ui', 'dist');
17
27
  app.use(express.static(uiDist));
@@ -1,9 +1,20 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import chalk from 'chalk';
3
4
  import { PATHS } from './config.js';
4
5
 
5
6
  export function appendLog(sessionId, entry) {
6
7
  const logFile = path.join(PATHS.logsDir, `session-${sessionId}.jsonl`);
7
8
  const line = JSON.stringify({ ts: new Date().toISOString(), sessionId, ...entry }) + '\n';
8
9
  fs.appendFileSync(logFile, line, 'utf8');
10
+
11
+ // Console output for better visibility
12
+ const statusColor = entry.status === 'ok' ? chalk.green : chalk.red;
13
+ console.log(
14
+ `[${chalk.dim(new Date().toLocaleTimeString())}] ` +
15
+ `${chalk.blue('Session')}: ${chalk.dim(sessionId.slice(0, 8))} | ` +
16
+ `${chalk.yellow('Iter')}: ${entry.iteration} | ` +
17
+ `${chalk.cyan('Status')}: ${statusColor(entry.status)} | ` +
18
+ `${entry.logSummary || '(no summary)'}`
19
+ );
9
20
  }