@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.
- package/docs/agent.md +16 -2
- package/docs/cli.md +2 -1
- package/docs/setup.md +45 -2
- package/docs/telegram.md +235 -0
- package/package.json +3 -1
- 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 +65 -12
- 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
|
@@ -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,
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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',
|