@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.
@@ -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, tools, toolDefs, prepareMessages) {
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
- while (true) {
248
- const run = await runAgentLoop(client, config, session, tools, toolDefs, prepareMessages);
249
- allToolCalls.push(...run.runToolCalls);
250
-
251
- if (run.status !== 'checkpoint_reached') {
252
- finalResponse = run.response;
253
- finalLogSummary = run.logSummary;
254
- finalStatus = run.status;
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
- const logEntry = {
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: allToolCalls,
261
- response: finalResponse,
262
- logSummary: finalLogSummary,
263
- status: finalStatus,
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
- // Check handoff limit
284
- session.metadata.handoffCount++;
285
- if (session.metadata.handoffCount > config.maxHandoffs) {
286
- finalResponse = run.response;
287
- finalLogSummary = run.logSummary;
288
- finalStatus = 'intervention_required';
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
- appendLog(sessionId, {
291
- iteration: 0,
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
- // Resume with checkpoint.remaining as new prompt
303
- session.messages.push({ role: 'user', content: run.checkpoint.remaining });
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
 
@@ -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
 
@@ -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',