@ducci/jarvis 1.0.8 → 1.0.10
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/cli.md +2 -1
- package/docs/setup.md +45 -2
- package/docs/telegram.md +235 -0
- package/package.json +4 -2
- package/src/channels/telegram/index.js +70 -0
- package/src/channels/telegram/sessions.js +18 -0
- package/src/index.js +43 -2
- package/src/scripts/onboarding.js +90 -20
- package/src/server/agent.js +108 -56
- package/src/server/app.js +2 -0
- package/src/server/config.js +4 -0
- package/src/server/tools.js +47 -0
- package/ui/dist/assets/{index-BFT9aOnN.js → index-DLrFBZmf.js} +3 -3
- package/ui/dist/index.html +1 -1
package/src/server/agent.js
CHANGED
|
@@ -62,7 +62,9 @@ async function callModelWithFallback(client, config, messages, tools) {
|
|
|
62
62
|
* Runs a single agent loop up to maxIterations.
|
|
63
63
|
* Returns { iteration, response, logSummary, status, runToolCalls, checkpoint }.
|
|
64
64
|
*/
|
|
65
|
-
async function runAgentLoop(client, config, session,
|
|
65
|
+
async function runAgentLoop(client, config, session, prepareMessages) {
|
|
66
|
+
let tools = loadTools();
|
|
67
|
+
let toolDefs = getToolDefinitions(tools);
|
|
66
68
|
let iteration = 0;
|
|
67
69
|
const runToolCalls = [];
|
|
68
70
|
let done = false;
|
|
@@ -85,7 +87,20 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
|
|
|
85
87
|
status: 'model_error',
|
|
86
88
|
runToolCalls,
|
|
87
89
|
checkpoint: null,
|
|
88
|
-
errorDetail: e.apiErrors ?? { message: e.message },
|
|
90
|
+
errorDetail: e.apiErrors ?? { message: e.message, stack: e.stack },
|
|
91
|
+
contextInfo: { messageCount: preparedMessages.length },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!modelResult.choices || modelResult.choices.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
iteration,
|
|
98
|
+
response: 'Model returned an empty response.',
|
|
99
|
+
logSummary: `Model error on iteration ${iteration}: Empty choices array.`,
|
|
100
|
+
status: 'model_error',
|
|
101
|
+
runToolCalls,
|
|
102
|
+
checkpoint: null,
|
|
103
|
+
errorDetail: { message: 'Empty choices array from LLM' },
|
|
89
104
|
contextInfo: { messageCount: preparedMessages.length },
|
|
90
105
|
};
|
|
91
106
|
}
|
|
@@ -100,6 +115,7 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
|
|
|
100
115
|
tool_calls: assistantMessage.tool_calls,
|
|
101
116
|
});
|
|
102
117
|
|
|
118
|
+
let toolsModified = false;
|
|
103
119
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
104
120
|
const toolName = toolCall.function.name;
|
|
105
121
|
let toolArgs;
|
|
@@ -118,6 +134,10 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
|
|
|
118
134
|
toolStatus = 'error';
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
if (toolName === 'save_tool' && toolStatus === 'ok') {
|
|
138
|
+
toolsModified = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
|
|
122
142
|
runToolCalls.push({ name: toolName, args: toolArgs, status: toolStatus, result: resultStr });
|
|
123
143
|
|
|
@@ -128,6 +148,12 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
|
|
|
128
148
|
});
|
|
129
149
|
}
|
|
130
150
|
|
|
151
|
+
// Reload tools if any were created/updated this iteration
|
|
152
|
+
if (toolsModified) {
|
|
153
|
+
tools = loadTools();
|
|
154
|
+
toolDefs = getToolDefinitions(tools);
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
continue;
|
|
132
158
|
}
|
|
133
159
|
|
|
@@ -168,7 +194,20 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
|
|
|
168
194
|
status: 'model_error',
|
|
169
195
|
runToolCalls,
|
|
170
196
|
checkpoint: null,
|
|
171
|
-
errorDetail: e.apiErrors ?? { message: e.message },
|
|
197
|
+
errorDetail: e.apiErrors ?? { message: e.message, stack: e.stack },
|
|
198
|
+
contextInfo: { messageCount: wrapUpMessages.length },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!wrapUpResult.choices || wrapUpResult.choices.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
iteration,
|
|
205
|
+
response: 'Wrap-up call returned an empty response.',
|
|
206
|
+
logSummary: 'Iteration limit reached. Wrap-up returned empty choices.',
|
|
207
|
+
status: 'model_error',
|
|
208
|
+
runToolCalls,
|
|
209
|
+
checkpoint: null,
|
|
210
|
+
errorDetail: { message: 'Empty choices array in wrap-up' },
|
|
172
211
|
contextInfo: { messageCount: wrapUpMessages.length },
|
|
173
212
|
};
|
|
174
213
|
}
|
|
@@ -225,9 +264,6 @@ export async function handleChat(config, requestSessionId, userMessage) {
|
|
|
225
264
|
session.messages.push({ role: 'user', content: userMessage });
|
|
226
265
|
session.metadata.handoffCount = 0;
|
|
227
266
|
|
|
228
|
-
const tools = loadTools();
|
|
229
|
-
const toolDefs = getToolDefinitions(tools);
|
|
230
|
-
|
|
231
267
|
// Resolves {{user_info}} in system prompt at runtime (never persisted)
|
|
232
268
|
function prepareMessages(messages) {
|
|
233
269
|
return messages.map((msg, i) => {
|
|
@@ -244,63 +280,79 @@ export async function handleChat(config, requestSessionId, userMessage) {
|
|
|
244
280
|
let finalStatus = 'ok';
|
|
245
281
|
|
|
246
282
|
// Handoff loop
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
try {
|
|
284
|
+
while (true) {
|
|
285
|
+
const run = await runAgentLoop(client, config, session, prepareMessages);
|
|
286
|
+
allToolCalls.push(...run.runToolCalls);
|
|
287
|
+
|
|
288
|
+
if (run.status !== 'checkpoint_reached') {
|
|
289
|
+
finalResponse = run.response;
|
|
290
|
+
finalLogSummary = run.logSummary;
|
|
291
|
+
finalStatus = run.status;
|
|
292
|
+
|
|
293
|
+
const logEntry = {
|
|
294
|
+
iteration: run.iteration,
|
|
295
|
+
model: config.selectedModel,
|
|
296
|
+
userInput: userMessage,
|
|
297
|
+
toolCalls: allToolCalls,
|
|
298
|
+
response: finalResponse,
|
|
299
|
+
logSummary: finalLogSummary,
|
|
300
|
+
status: finalStatus,
|
|
301
|
+
};
|
|
302
|
+
if (run.errorDetail) logEntry.errorDetail = run.errorDetail;
|
|
303
|
+
if (run.contextInfo) logEntry.contextInfo = run.contextInfo;
|
|
304
|
+
if (run.rawResponse) logEntry.rawResponse = run.rawResponse;
|
|
305
|
+
appendLog(sessionId, logEntry);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
255
308
|
|
|
256
|
-
|
|
309
|
+
// Checkpoint reached — log this run
|
|
310
|
+
appendLog(sessionId, {
|
|
257
311
|
iteration: run.iteration,
|
|
258
312
|
model: config.selectedModel,
|
|
259
313
|
userInput: userMessage,
|
|
260
|
-
toolCalls:
|
|
261
|
-
response:
|
|
262
|
-
logSummary:
|
|
263
|
-
status:
|
|
264
|
-
};
|
|
265
|
-
if (run.errorDetail) logEntry.errorDetail = run.errorDetail;
|
|
266
|
-
if (run.contextInfo) logEntry.contextInfo = run.contextInfo;
|
|
267
|
-
if (run.rawResponse) logEntry.rawResponse = run.rawResponse;
|
|
268
|
-
appendLog(sessionId, logEntry);
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Checkpoint reached — log this run
|
|
273
|
-
appendLog(sessionId, {
|
|
274
|
-
iteration: run.iteration,
|
|
275
|
-
model: config.selectedModel,
|
|
276
|
-
userInput: userMessage,
|
|
277
|
-
toolCalls: run.runToolCalls,
|
|
278
|
-
response: run.response,
|
|
279
|
-
logSummary: run.logSummary,
|
|
280
|
-
status: 'checkpoint_reached',
|
|
281
|
-
});
|
|
314
|
+
toolCalls: run.runToolCalls,
|
|
315
|
+
response: run.response,
|
|
316
|
+
logSummary: run.logSummary,
|
|
317
|
+
status: 'checkpoint_reached',
|
|
318
|
+
});
|
|
282
319
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
320
|
+
// Check handoff limit
|
|
321
|
+
session.metadata.handoffCount++;
|
|
322
|
+
if (session.metadata.handoffCount > config.maxHandoffs) {
|
|
323
|
+
finalResponse = run.response;
|
|
324
|
+
finalLogSummary = run.logSummary;
|
|
325
|
+
finalStatus = 'intervention_required';
|
|
326
|
+
|
|
327
|
+
appendLog(sessionId, {
|
|
328
|
+
iteration: 0,
|
|
329
|
+
model: config.selectedModel,
|
|
330
|
+
userInput: userMessage,
|
|
331
|
+
toolCalls: [],
|
|
332
|
+
response: finalResponse,
|
|
333
|
+
logSummary: 'Max handoffs exceeded. Human intervention required.',
|
|
334
|
+
status: 'intervention_required',
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
289
338
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
model: config.selectedModel,
|
|
293
|
-
userInput: userMessage,
|
|
294
|
-
toolCalls: [],
|
|
295
|
-
response: finalResponse,
|
|
296
|
-
logSummary: 'Max handoffs exceeded. Human intervention required.',
|
|
297
|
-
status: 'intervention_required',
|
|
298
|
-
});
|
|
299
|
-
break;
|
|
339
|
+
// Resume with checkpoint.remaining as new prompt
|
|
340
|
+
session.messages.push({ role: 'user', content: run.checkpoint.remaining });
|
|
300
341
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
342
|
+
} catch (e) {
|
|
343
|
+
const errorLog = {
|
|
344
|
+
iteration: 0,
|
|
345
|
+
model: config.selectedModel,
|
|
346
|
+
userInput: userMessage,
|
|
347
|
+
toolCalls: allToolCalls,
|
|
348
|
+
response: `An unexpected server error occurred: ${e.message}`,
|
|
349
|
+
logSummary: `Critical error: ${e.message}`,
|
|
350
|
+
status: 'error',
|
|
351
|
+
errorDetail: { message: e.message, stack: e.stack },
|
|
352
|
+
};
|
|
353
|
+
appendLog(sessionId, errorLog);
|
|
354
|
+
// Re-throw to let app.js handle the HTTP response
|
|
355
|
+
throw e;
|
|
304
356
|
}
|
|
305
357
|
|
|
306
358
|
saveSession(sessionId, session);
|
package/src/server/app.js
CHANGED
|
@@ -6,6 +6,7 @@ import { realpathSync } from 'fs';
|
|
|
6
6
|
import { loadConfig, ensureDirectories } from './config.js';
|
|
7
7
|
import { seedTools } from './tools.js';
|
|
8
8
|
import { handleChat } from './agent.js';
|
|
9
|
+
import { startTelegramChannel } from '../channels/telegram/index.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -74,6 +75,7 @@ function startServer() {
|
|
|
74
75
|
const PORT = config.port;
|
|
75
76
|
app.listen(PORT, () => {
|
|
76
77
|
console.log(`Jarvis server listening on port ${PORT}`);
|
|
78
|
+
startTelegramChannel(config);
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
|
package/src/server/config.js
CHANGED
|
@@ -49,6 +49,10 @@ export function loadConfig() {
|
|
|
49
49
|
maxIterations: settings.maxIterations || 10,
|
|
50
50
|
maxHandoffs: settings.maxHandoffs || 5,
|
|
51
51
|
port: settings.port || 18008,
|
|
52
|
+
telegram: {
|
|
53
|
+
token: process.env.TELEGRAM_BOT_TOKEN || null,
|
|
54
|
+
allowedUserIds: settings.channels?.telegram?.allowedUserIds || [],
|
|
55
|
+
},
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
|
package/src/server/tools.js
CHANGED
|
@@ -90,6 +90,53 @@ const SEED_TOOLS = {
|
|
|
90
90
|
},
|
|
91
91
|
code: `const filePath = path.join(process.env.HOME, '.jarvis/data/user-info.json'); const raw = await fs.promises.readFile(filePath, 'utf8').catch(() => '{"items":[]}'); const { items } = JSON.parse(raw); return { status: 'ok', items };`,
|
|
92
92
|
},
|
|
93
|
+
save_tool: {
|
|
94
|
+
definition: {
|
|
95
|
+
type: 'function',
|
|
96
|
+
function: {
|
|
97
|
+
name: 'save_tool',
|
|
98
|
+
description: 'Create or update a custom tool and make it available immediately in this session. Use this to build reusable JS tools for tasks you repeat. The tool code runs in Node.js and has access to: args, fs, path, process, require.',
|
|
99
|
+
parameters: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
name: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Tool name in snake_case (e.g. "parse_json_file"). Must be unique.',
|
|
105
|
+
},
|
|
106
|
+
description: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'What the tool does. Be specific — the LLM uses this to decide when to call it.',
|
|
109
|
+
},
|
|
110
|
+
parameters: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
description: 'JSON Schema object for the tool parameters (with type, properties, required fields).',
|
|
113
|
+
},
|
|
114
|
+
code: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'The body of an async function. Must end with a return statement — the returned value becomes the tool result. Available bindings: args (your tool parameters), fs (node:fs), path (node:path), process, require. Do NOT wrap in a function declaration. Example: const raw = await fs.promises.readFile(args.filePath, "utf8"); const data = JSON.parse(raw); return { count: data.length, first: data[0] };',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ['name', 'description', 'parameters', 'code'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
code: `const toolsFile = path.join(process.env.HOME, '.jarvis/data/tools/tools.json'); const raw = await fs.promises.readFile(toolsFile, 'utf8').catch(() => '{}'); const tools = JSON.parse(raw); tools[args.name] = { definition: { type: 'function', function: { name: args.name, description: args.description, parameters: args.parameters } }, code: args.code }; await fs.promises.writeFile(toolsFile, JSON.stringify(tools, null, 2), 'utf8'); return { status: 'ok', saved: args.name };`,
|
|
124
|
+
},
|
|
125
|
+
list_tools: {
|
|
126
|
+
definition: {
|
|
127
|
+
type: 'function',
|
|
128
|
+
function: {
|
|
129
|
+
name: 'list_tools',
|
|
130
|
+
description: 'List all available tools with their names and descriptions. Use this to see what tools exist before creating a new one.',
|
|
131
|
+
parameters: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {},
|
|
134
|
+
required: [],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
code: `const toolsFile = path.join(process.env.HOME, '.jarvis/data/tools/tools.json'); const raw = await fs.promises.readFile(toolsFile, 'utf8').catch(() => '{}'); const tools = JSON.parse(raw); const list = Object.entries(tools).map(([name, t]) => ({ name, description: t.definition.function.description })); return { status: 'ok', tools: list };`,
|
|
139
|
+
},
|
|
93
140
|
get_recent_sessions: {
|
|
94
141
|
definition: {
|
|
95
142
|
type: 'function',
|