@ducci/jarvis 1.0.9 → 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.
@@ -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
 
@@ -62,7 +64,9 @@ async function callModelWithFallback(client, config, messages, tools) {
62
64
  * Runs a single agent loop up to maxIterations.
63
65
  * Returns { iteration, response, logSummary, status, runToolCalls, checkpoint }.
64
66
  */
65
- async function runAgentLoop(client, config, session, tools, toolDefs, prepareMessages) {
67
+ async function runAgentLoop(client, config, session, prepareMessages) {
68
+ let tools = loadTools();
69
+ let toolDefs = getToolDefinitions(tools);
66
70
  let iteration = 0;
67
71
  const runToolCalls = [];
68
72
  let done = false;
@@ -110,9 +114,16 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
110
114
  session.messages.push({
111
115
  role: 'assistant',
112
116
  content: assistantMessage.content || null,
113
- 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
+ })),
114
124
  });
115
125
 
126
+ let toolsModified = false;
116
127
  for (const toolCall of assistantMessage.tool_calls) {
117
128
  const toolName = toolCall.function.name;
118
129
  let toolArgs;
@@ -131,6 +142,10 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
131
142
  toolStatus = 'error';
132
143
  }
133
144
 
145
+ if (toolName === 'save_tool' && toolStatus === 'ok') {
146
+ toolsModified = true;
147
+ }
148
+
134
149
  const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
135
150
  runToolCalls.push({ name: toolName, args: toolArgs, status: toolStatus, result: resultStr });
136
151
 
@@ -141,24 +156,55 @@ async function runAgentLoop(client, config, session, tools, toolDefs, prepareMes
141
156
  });
142
157
  }
143
158
 
159
+ // Reload tools if any were created/updated this iteration
160
+ if (toolsModified) {
161
+ tools = loadTools();
162
+ toolDefs = getToolDefinitions(tools);
163
+ }
164
+
144
165
  continue;
145
166
  }
146
167
 
147
168
  // No tool calls — final response
148
- const content = assistantMessage.content || '';
149
- 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;
150
172
 
151
173
  try {
152
- const parsed = JSON.parse(content);
153
- response = parsed.response || content;
154
- logSummary = parsed.logSummary || '';
174
+ parsed = JSON.parse(content);
155
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
156
198
  response = content;
157
- logSummary = 'Model returned non-JSON final response.';
199
+ logSummary = 'Model returned non-JSON final response after recovery attempts.';
158
200
  status = 'format_error';
159
201
  return { iteration, response, logSummary, status, runToolCalls, checkpoint: null, rawResponse: content };
160
202
  }
161
203
 
204
+ session.messages.push({ role: 'assistant', content });
205
+ response = parsed.response || content;
206
+ logSummary = parsed.logSummary || '';
207
+
162
208
  done = true;
163
209
  break;
164
210
  }
@@ -251,9 +297,6 @@ export async function handleChat(config, requestSessionId, userMessage) {
251
297
  session.messages.push({ role: 'user', content: userMessage });
252
298
  session.metadata.handoffCount = 0;
253
299
 
254
- const tools = loadTools();
255
- const toolDefs = getToolDefinitions(tools);
256
-
257
300
  // Resolves {{user_info}} in system prompt at runtime (never persisted)
258
301
  function prepareMessages(messages) {
259
302
  return messages.map((msg, i) => {
@@ -272,7 +315,7 @@ export async function handleChat(config, requestSessionId, userMessage) {
272
315
  // Handoff loop
273
316
  try {
274
317
  while (true) {
275
- const run = await runAgentLoop(client, config, session, tools, toolDefs, prepareMessages);
318
+ const run = await runAgentLoop(client, config, session, prepareMessages);
276
319
  allToolCalls.push(...run.runToolCalls);
277
320
 
278
321
  if (run.status !== 'checkpoint_reached') {
@@ -293,6 +336,16 @@ export async function handleChat(config, requestSessionId, userMessage) {
293
336
  if (run.contextInfo) logEntry.contextInfo = run.contextInfo;
294
337
  if (run.rawResponse) logEntry.rawResponse = run.rawResponse;
295
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
+
296
349
  break;
297
350
  }
298
351
 
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',