@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 +2 -2
- package/src/server/agent.js +124 -53
- package/src/server/app.js +10 -0
- package/src/server/logging.js +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ducci/jarvis",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
"
|
|
23
|
+
"release": "node scripts/publish.js",
|
|
24
24
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
package/src/server/agent.js
CHANGED
|
@@ -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 (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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:
|
|
238
|
-
response:
|
|
239
|
-
logSummary:
|
|
240
|
-
status:
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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));
|
package/src/server/logging.js
CHANGED
|
@@ -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
|
}
|