@ducci/jarvis 1.0.47 → 1.0.49
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/system-prompt.md +1 -1
- package/package.json +1 -1
- package/src/channels/telegram/index.js +2 -0
- package/src/scripts/onboarding.js +47 -0
- package/src/server/agent.js +106 -21
- package/src/server/logging.js +0 -1
- package/src/server/start.js +8 -0
- package/src/server/tools.js +32 -0
package/docs/system-prompt.md
CHANGED
|
@@ -45,7 +45,7 @@ There are two types of responses depending on whether you need to use tools:
|
|
|
45
45
|
"logSummary": "A concise explanation of what you did and why, written for a human reading the logs."
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
The `response` value must be a string — never an array or object. Use HTML formatting tags for readability: <b>bold</b>, <i>italic</i>, <code>inline code</code>, <pre>code
|
|
48
|
+
The `response` value must be a string — never an array or object. Use HTML formatting tags for readability — only these Telegram-supported tags are allowed: <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strikethrough</s>, <code>inline code</code>, <pre>code block</pre>, <blockquote>quote</blockquote>, <a href="URL">link</a>. For line breaks use actual newlines (\n), never <br>. Never use Markdown formatting (no **, __, `, or ```). If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
49
49
|
|
|
50
50
|
Never include markdown code fences, preamble, or any text outside this JSON object. If you cannot complete a task, explain why in the `response` field — still as valid JSON.
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -9,6 +9,8 @@ import { load, save } from './sessions.js';
|
|
|
9
9
|
|
|
10
10
|
async function sendMessage(api, chatId, text, sessionId) {
|
|
11
11
|
const MAX_TG = 4096;
|
|
12
|
+
// Telegram HTML mode does not support <br> — replace with newlines before sending
|
|
13
|
+
text = text.replace(/<br\s*\/?>/gi, '\n');
|
|
12
14
|
const chunks = [];
|
|
13
15
|
for (let i = 0; i < text.length; i += MAX_TG) {
|
|
14
16
|
chunks.push(text.slice(i, i + MAX_TG));
|
|
@@ -3,9 +3,25 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import { createRequire } from 'module';
|
|
6
8
|
import inquirer from 'inquirer';
|
|
7
9
|
import chalk from 'chalk';
|
|
8
10
|
|
|
11
|
+
// Resolve pm2 CLI from local node_modules so it works even when pm2 is not in PATH
|
|
12
|
+
// (e.g. when Node is managed by NVM and the shell is not initialised with it).
|
|
13
|
+
const _require = createRequire(import.meta.url);
|
|
14
|
+
let PM2_BIN;
|
|
15
|
+
try {
|
|
16
|
+
PM2_BIN = _require.resolve('pm2/bin/pm2');
|
|
17
|
+
} catch {
|
|
18
|
+
PM2_BIN = null; // will fall back gracefully
|
|
19
|
+
}
|
|
20
|
+
function pm2(...args) {
|
|
21
|
+
if (!PM2_BIN) return { status: 1, error: new Error('pm2 binary not found') };
|
|
22
|
+
return spawnSync(process.execPath, [PM2_BIN, ...args], { stdio: 'inherit' });
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
const jarvisDir = path.join(os.homedir(), '.jarvis');
|
|
10
26
|
const envFile = path.join(jarvisDir, '.env');
|
|
11
27
|
const configDir = path.join(jarvisDir, 'data', 'config');
|
|
@@ -420,6 +436,37 @@ async function run() {
|
|
|
420
436
|
}
|
|
421
437
|
}
|
|
422
438
|
|
|
439
|
+
// --- LOG ROTATION STEP ---
|
|
440
|
+
const logrotateModuleDir = path.join(os.homedir(), '.pm2', 'modules', 'pm2-logrotate');
|
|
441
|
+
const logrotateInstalled = fs.existsSync(logrotateModuleDir);
|
|
442
|
+
|
|
443
|
+
if (logrotateInstalled) {
|
|
444
|
+
console.log(chalk.green('pm2-logrotate is already installed — server.log will be rotated automatically.'));
|
|
445
|
+
} else {
|
|
446
|
+
const { setupLogrotate } = await inquirer.prompt([
|
|
447
|
+
{
|
|
448
|
+
type: 'confirm',
|
|
449
|
+
name: 'setupLogrotate',
|
|
450
|
+
message: 'Install pm2-logrotate to prevent server.log from growing indefinitely?',
|
|
451
|
+
default: true,
|
|
452
|
+
}
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
if (setupLogrotate) {
|
|
456
|
+
console.log(chalk.blue('Installing pm2-logrotate...'));
|
|
457
|
+
const install = pm2('install', 'pm2-logrotate');
|
|
458
|
+
if (install.status !== 0) {
|
|
459
|
+
console.log(chalk.yellow('Installation failed — try running `pm2 install pm2-logrotate` manually.'));
|
|
460
|
+
} else {
|
|
461
|
+
pm2('set', 'pm2-logrotate:max_size', '10M');
|
|
462
|
+
pm2('set', 'pm2-logrotate:retain', '5');
|
|
463
|
+
pm2('set', 'pm2-logrotate:compress', 'true');
|
|
464
|
+
pm2('set', 'pm2-logrotate:rotateInterval', '0 0 * * *');
|
|
465
|
+
console.log(chalk.green('pm2-logrotate installed: 10 MB max, 5 rotated files kept, daily rotation.'));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
423
470
|
console.log(chalk.green.bold('\nSetup complete!'));
|
|
424
471
|
}
|
|
425
472
|
|
package/src/server/agent.js
CHANGED
|
@@ -105,6 +105,72 @@ function hasConsecutiveModelErrors(messages) {
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Runs a subagent in its own isolated session for a single self-contained task.
|
|
110
|
+
* Called when the parent agent invokes the spawn_subagent tool.
|
|
111
|
+
*/
|
|
112
|
+
async function runSubagent(client, config, args, parentSessionId) {
|
|
113
|
+
const subSessionId = `sub-${crypto.randomUUID()}`;
|
|
114
|
+
const systemPromptTemplate = loadSystemPrompt();
|
|
115
|
+
const subSession = createSession(systemPromptTemplate);
|
|
116
|
+
|
|
117
|
+
let userContent = args.prompt;
|
|
118
|
+
if (args.context) {
|
|
119
|
+
userContent = `[Context: ${args.context}]\n\n${args.prompt}`;
|
|
120
|
+
}
|
|
121
|
+
subSession.messages.push({ role: 'user', content: userContent });
|
|
122
|
+
|
|
123
|
+
const subConfig = {
|
|
124
|
+
...config,
|
|
125
|
+
excludeTools: ['spawn_subagent'],
|
|
126
|
+
maxIterations: args.maxIterations || config.maxIterations,
|
|
127
|
+
_sessionId: subSessionId,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const usageAccum = { prompt: 0, completion: 0, cacheRead: 0, cacheCreation: 0 };
|
|
131
|
+
|
|
132
|
+
function prepareMessages(messages) {
|
|
133
|
+
const resolved = messages.map((msg, i) => {
|
|
134
|
+
if (i === 0 && msg.role === 'system') {
|
|
135
|
+
return { ...msg, content: resolveSystemPrompt(msg.content, subSessionId) };
|
|
136
|
+
}
|
|
137
|
+
return msg;
|
|
138
|
+
});
|
|
139
|
+
if (resolved.length <= subConfig.contextWindow + 1) return resolved;
|
|
140
|
+
return [resolved[0], ...resolved.slice(-(subConfig.contextWindow))];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const run = await runAgentLoop(client, subConfig, subSession, prepareMessages, usageAccum);
|
|
144
|
+
|
|
145
|
+
await appendLog(subSessionId, {
|
|
146
|
+
iteration: run.iteration,
|
|
147
|
+
model: config.selectedModel,
|
|
148
|
+
userInput: args.prompt,
|
|
149
|
+
toolCalls: run.runToolCalls,
|
|
150
|
+
response: run.response,
|
|
151
|
+
logSummary: run.logSummary,
|
|
152
|
+
status: run.status,
|
|
153
|
+
parentSessionId: parentSessionId || null,
|
|
154
|
+
label: args.label || null,
|
|
155
|
+
tokenUsage: { ...usageAccum },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
subSession.metadata.tokenUsage = { ...usageAccum };
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await saveSession(subSessionId, subSession);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error(`Failed to save subagent session ${subSessionId}:`, e);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
status: 'ok',
|
|
168
|
+
response: run.response,
|
|
169
|
+
runStatus: run.status,
|
|
170
|
+
sessionId: subSessionId,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
108
174
|
/**
|
|
109
175
|
* Runs a single agent loop up to maxIterations.
|
|
110
176
|
* Returns { iteration, response, logSummary, status, runToolCalls, checkpoint }.
|
|
@@ -112,6 +178,9 @@ function hasConsecutiveModelErrors(messages) {
|
|
|
112
178
|
export async function runAgentLoop(client, config, session, prepareMessages, usageAccum) {
|
|
113
179
|
let tools = await loadTools();
|
|
114
180
|
let toolDefs = getToolDefinitions(tools);
|
|
181
|
+
if (config.excludeTools?.length) {
|
|
182
|
+
toolDefs = toolDefs.filter(t => !config.excludeTools.includes(t.function?.name));
|
|
183
|
+
}
|
|
115
184
|
let iteration = 0;
|
|
116
185
|
const runToolCalls = [];
|
|
117
186
|
const loopTracker = new Map();
|
|
@@ -162,7 +231,7 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
162
231
|
|
|
163
232
|
const assistantMessage = modelResult.choices[0].message;
|
|
164
233
|
|
|
165
|
-
// Tool calls present — execute
|
|
234
|
+
// Tool calls present — execute in parallel, then process results in order
|
|
166
235
|
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
|
167
236
|
session.messages.push({
|
|
168
237
|
role: 'assistant',
|
|
@@ -176,17 +245,42 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
176
245
|
})),
|
|
177
246
|
});
|
|
178
247
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
248
|
+
// Execute all tool calls concurrently; session mutations happen serially below.
|
|
249
|
+
const toolResults = await Promise.all(
|
|
250
|
+
assistantMessage.tool_calls.map(async (toolCall) => {
|
|
251
|
+
const toolName = toolCall.function.name;
|
|
252
|
+
let toolArgs;
|
|
253
|
+
let argParseError = null;
|
|
254
|
+
try {
|
|
255
|
+
toolArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
256
|
+
} catch (e) {
|
|
257
|
+
argParseError = e;
|
|
258
|
+
}
|
|
189
259
|
|
|
260
|
+
if (argParseError) {
|
|
261
|
+
return { toolCall, toolName, toolArgs: {}, argParseError, result: null, toolStatus: 'error' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let result;
|
|
265
|
+
let toolStatus = 'ok';
|
|
266
|
+
try {
|
|
267
|
+
if (toolName === 'spawn_subagent') {
|
|
268
|
+
result = await runSubagent(client, config, toolArgs, config._sessionId);
|
|
269
|
+
} else {
|
|
270
|
+
result = await executeTool(tools, toolName, toolArgs);
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
result = { status: 'error', error: e.message };
|
|
274
|
+
toolStatus = 'error';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { toolCall, toolName, toolArgs, argParseError: null, result, toolStatus };
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Process results serially to preserve message order and update trackers.
|
|
282
|
+
let stderrErrorInIteration = false;
|
|
283
|
+
for (const { toolCall, toolName, toolArgs, argParseError, result, toolStatus } of toolResults) {
|
|
190
284
|
if (argParseError) {
|
|
191
285
|
const errorContent = JSON.stringify({
|
|
192
286
|
status: 'error',
|
|
@@ -198,15 +292,6 @@ export async function runAgentLoop(client, config, session, prepareMessages, usa
|
|
|
198
292
|
continue;
|
|
199
293
|
}
|
|
200
294
|
|
|
201
|
-
let result;
|
|
202
|
-
let toolStatus = 'ok';
|
|
203
|
-
try {
|
|
204
|
-
result = await executeTool(tools, toolName, toolArgs);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
result = { status: 'error', error: e.message };
|
|
207
|
-
toolStatus = 'error';
|
|
208
|
-
}
|
|
209
|
-
|
|
210
295
|
const resultObj = typeof result === 'object' && result !== null ? result : null;
|
|
211
296
|
const toolFailed = toolStatus === 'error' || (resultObj && resultObj.status === 'error');
|
|
212
297
|
if (toolFailed) {
|
|
@@ -620,7 +705,7 @@ async function _runHandleChat(config, sessionId, userMessage, attachments = [])
|
|
|
620
705
|
}
|
|
621
706
|
|
|
622
707
|
const runStartIndex = session.messages.length;
|
|
623
|
-
const run = await runAgentLoop(client, config, session, prepareMessages, usageAccum);
|
|
708
|
+
const run = await runAgentLoop(client, { ...config, _sessionId: sessionId }, session, prepareMessages, usageAccum);
|
|
624
709
|
allToolCalls.push(...run.runToolCalls);
|
|
625
710
|
|
|
626
711
|
if (run.status !== 'checkpoint_reached') {
|
package/src/server/logging.js
CHANGED
|
@@ -11,7 +11,6 @@ export async function appendLog(sessionId, entry) {
|
|
|
11
11
|
// Console output for better visibility
|
|
12
12
|
const statusColor = entry.status === 'ok' ? chalk.green : chalk.red;
|
|
13
13
|
console.log(
|
|
14
|
-
`[${chalk.dim(new Date().toLocaleTimeString())}] ` +
|
|
15
14
|
`${chalk.blue('Session')}: ${chalk.dim(sessionId.slice(0, 8))} | ` +
|
|
16
15
|
`${chalk.yellow('Iter')}: ${entry.iteration} | ` +
|
|
17
16
|
`${chalk.cyan('Status')}: ${statusColor(entry.status)} | ` +
|
package/src/server/start.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// Prefix every console.log/error line with a date+time stamp so all output
|
|
2
|
+
// (agent, cron, telegram, tools, etc.) is consistently timestamped in server.log.
|
|
3
|
+
const _log = console.log.bind(console);
|
|
4
|
+
const _err = console.error.bind(console);
|
|
5
|
+
const ts = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
6
|
+
console.log = (...args) => _log(`[${ts()}]`, ...args);
|
|
7
|
+
console.error = (...args) => _err(`[${ts()}]`, ...args);
|
|
8
|
+
|
|
1
9
|
import { startServer } from './app.js';
|
|
2
10
|
|
|
3
11
|
startServer();
|
package/src/server/tools.js
CHANGED
|
@@ -584,6 +584,38 @@ const SEED_TOOLS = {
|
|
|
584
584
|
return { status: 'ok', entries };
|
|
585
585
|
`,
|
|
586
586
|
},
|
|
587
|
+
spawn_subagent: {
|
|
588
|
+
definition: {
|
|
589
|
+
type: 'function',
|
|
590
|
+
function: {
|
|
591
|
+
name: 'spawn_subagent',
|
|
592
|
+
description: 'Spawn an independent subagent to handle a single subtask in its own isolated context and session. Use this when processing many similar items (e.g. emails, files, URLs) where doing them serially in the same context would overflow. Each subagent runs a full agent loop with access to all tools and returns its final response. Multiple spawn_subagent calls in a single response run in parallel. The subagent has no access to the current conversation — the prompt must be fully self-contained. Do not instruct subagents to use send_telegram_message; collect their results and notify the user yourself.',
|
|
593
|
+
parameters: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: {
|
|
596
|
+
prompt: {
|
|
597
|
+
type: 'string',
|
|
598
|
+
description: 'The self-contained task for the subagent. Must include all necessary context — the subagent has no access to the current conversation history.',
|
|
599
|
+
},
|
|
600
|
+
context: {
|
|
601
|
+
type: 'string',
|
|
602
|
+
description: 'Optional extra context to prepend to the prompt (e.g. the item to process, such as an email body or file path).',
|
|
603
|
+
},
|
|
604
|
+
label: {
|
|
605
|
+
type: 'string',
|
|
606
|
+
description: 'Optional short label for this subagent, used in logging (e.g. "email-42", "file-scan-/tmp/foo.txt").',
|
|
607
|
+
},
|
|
608
|
+
maxIterations: {
|
|
609
|
+
type: 'number',
|
|
610
|
+
description: 'Optional cap on the number of iterations the subagent may use. Defaults to the global maxIterations setting. Use a lower value (e.g. 5) for simple subtasks in bulk processing.',
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
required: ['prompt'],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
code: `return { status: 'error', error: 'spawn_subagent is a native tool handled by the agent runtime.' };`,
|
|
618
|
+
},
|
|
587
619
|
read_skill: {
|
|
588
620
|
definition: {
|
|
589
621
|
type: 'function',
|