naisys 1.4.0 → 1.5.0
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/README.md +16 -6
- package/bin/naisys +7 -4
- package/bin/trimsession +3 -0
- package/dist/command/commandHandler.js +31 -14
- package/dist/command/commandLoop.js +47 -16
- package/dist/command/promptBuilder.js +2 -1
- package/dist/command/shellCommand.js +26 -33
- package/dist/command/shellWrapper.js +74 -80
- package/dist/config.js +25 -16
- package/dist/{apps → features}/genimg.js +17 -15
- package/dist/{apps → features}/llmail.js +72 -17
- package/dist/{apps → features}/llmynx.js +40 -14
- package/dist/features/subagent.js +223 -0
- package/dist/features/workspaces.js +50 -0
- package/dist/llm/contextManager.js +117 -82
- package/dist/llm/costTracker.js +30 -4
- package/dist/llm/dreamMaker.js +22 -16
- package/dist/llm/llModels.js +22 -4
- package/dist/llm/llmDtos.js +7 -0
- package/dist/llm/llmService.js +9 -9
- package/dist/llm/systemMessage.js +80 -0
- package/dist/utils/agentNames.js +62 -0
- package/dist/utils/dbUtils.js +7 -5
- package/dist/utils/logService.js +16 -21
- package/dist/utils/output.js +1 -0
- package/dist/utils/pathService.js +66 -0
- package/dist/utils/utilities.js +14 -30
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ dreamModel: claude3opus
|
|
|
73
73
|
|
|
74
74
|
# The model to use for llmynx, pre-processing websites to fit into a smaller context (use a cheaper model)
|
|
75
75
|
# defaults to the shellModel if omitted
|
|
76
|
-
webModel:
|
|
76
|
+
webModel: claude3haiku
|
|
77
77
|
|
|
78
78
|
# The model used by the 'genimg' command. If not defined then the genimg command is not available to the LLM
|
|
79
79
|
# Valid values: dalle2-256, dalle2-512, dalle2-1024, dalle3-1024, dalle3-1024-HD
|
|
@@ -112,6 +112,11 @@ spendLimitDollars: 2.00
|
|
|
112
112
|
# Auto: All commands are run through the separate LLM instace that will check to see if the command is safe
|
|
113
113
|
commandProtection: "none"
|
|
114
114
|
|
|
115
|
+
# The max number of subagents allowed to be started and managed. Leave out to disable.
|
|
116
|
+
# Costs by the subagent are applied to the host agent's spend limit
|
|
117
|
+
# Careful: Sub-agents can be chatty, slowing down progress.
|
|
118
|
+
subagentMax: 0
|
|
119
|
+
|
|
115
120
|
# Run these commands on session start, in the example below the agent will see how to use mail and a list of other agents
|
|
116
121
|
initialCommands:
|
|
117
122
|
- llmail users
|
|
@@ -169,7 +174,8 @@ initialCommands:
|
|
|
169
174
|
- NAISYS apps
|
|
170
175
|
- `llmail` - A context friendly 'mail system' used for agent to agent communication
|
|
171
176
|
- `llmynx` - A context friendly wrapping on the lynx browser that can use a separate LLM to reduce the size of a large webpage into something that can fit into the LLM's context
|
|
172
|
-
- `genimg "<description>" <filepath>` - Generates an image with the given description, save at the specified path
|
|
177
|
+
- `genimg "<description>" <filepath>` - Generates an image with the given description, save at the specified fully qualified path
|
|
178
|
+
- `subagent` - A way for LLMs to start/stop their own sub-agents. Communicating with each other with `llmail`. Set the `subagentMax` in the agent config to enable.
|
|
173
179
|
|
|
174
180
|
## Running NAISYS from Source
|
|
175
181
|
|
|
@@ -190,14 +196,18 @@ initialCommands:
|
|
|
190
196
|
- Install WSL (Windows Subsystem for Linux)
|
|
191
197
|
- The `NAISYS_FOLDER` and `WEBSITE_FOLDER` should be set to the WSL path
|
|
192
198
|
- So `C:\var\naisys` should be `/mnt/c/var/naisys` in the `.env` file
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
|
|
200
|
+
#### Using NAISYS for a website
|
|
201
|
+
|
|
202
|
+
- Many frameworks come with their own dev server
|
|
203
|
+
- PHP for example can start a server with `php -S localhost:8000 -d display_errors=On -d error_reporting=E_ALL`
|
|
204
|
+
- Start the server and put the URL in the `.env` file
|
|
196
205
|
|
|
197
206
|
## Changelog
|
|
198
207
|
|
|
208
|
+
- 1.5: Allow agents to start their own parallel `subagents`
|
|
199
209
|
- 1.4: `genimg` command for generating images
|
|
200
210
|
- 1.3: Post-session 'dreaming' as well as a mail 'blackout' period
|
|
201
211
|
- 1.2: Created stand-in shell commands for custom Naisys commands
|
|
202
212
|
- 1.1: Added command protection settings to prevent unwanted writes
|
|
203
|
-
- 1.0: Initial release
|
|
213
|
+
- 1.0: Initial release
|
package/bin/naisys
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# Make sure to enable this script for execution with `chmod +x naisys`
|
|
4
4
|
|
|
5
|
+
# Resolves the location of naisys from the bin directory
|
|
6
|
+
SCRIPT=$(readlink -f "$0" || echo "$0")
|
|
7
|
+
SCRIPT_DIR=$(dirname "$SCRIPT")/..
|
|
8
|
+
|
|
5
9
|
# Check if an argument is provided
|
|
6
10
|
if [ $# -eq 0 ]; then
|
|
11
|
+
# get version from package.json
|
|
12
|
+
VERSION=$(node -e "console.log(require('${SCRIPT_DIR}/package.json').version)")
|
|
7
13
|
echo "NAISYS: Node.js Autonomous Intelligence System"
|
|
14
|
+
echo " Version: $VERSION"
|
|
8
15
|
echo " Usage: naisys <path to agent config yaml, or directory>"
|
|
9
16
|
echo " Note: If a folder is passed then all agents will be started in a tmux session"
|
|
10
17
|
exit 1
|
|
11
18
|
fi
|
|
12
19
|
|
|
13
|
-
# Resolves the location of naisys from the bin directory
|
|
14
|
-
SCRIPT=$(readlink -f "$0" || echo "$0")
|
|
15
|
-
SCRIPT_DIR=$(dirname "$SCRIPT")/..
|
|
16
|
-
|
|
17
20
|
# if path is a yaml file then start a single agent
|
|
18
21
|
if [ -f "$1" ]; then
|
|
19
22
|
if [[ "$1" == *".yaml" ]]; then
|
package/bin/trimsession
ADDED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import * as genimg from "../apps/genimg.js";
|
|
3
|
-
import * as llmail from "../apps/llmail.js";
|
|
4
|
-
import * as llmynx from "../apps/llmynx.js";
|
|
5
2
|
import * as config from "../config.js";
|
|
3
|
+
import * as genimg from "../features/genimg.js";
|
|
4
|
+
import * as llmail from "../features/llmail.js";
|
|
5
|
+
import * as llmynx from "../features/llmynx.js";
|
|
6
|
+
import * as subagent from "../features/subagent.js";
|
|
6
7
|
import * as contextManager from "../llm/contextManager.js";
|
|
7
|
-
import { ContentSource } from "../llm/contextManager.js";
|
|
8
8
|
import * as costTracker from "../llm/costTracker.js";
|
|
9
9
|
import * as dreamMaker from "../llm/dreamMaker.js";
|
|
10
|
+
import { ContentSource } from "../llm/llmDtos.js";
|
|
10
11
|
import * as inputMode from "../utils/inputMode.js";
|
|
11
12
|
import { InputMode } from "../utils/inputMode.js";
|
|
12
13
|
import * as output from "../utils/output.js";
|
|
@@ -65,7 +66,18 @@ export async function processCommand(prompt, consoleInput) {
|
|
|
65
66
|
await contextManager.append("Comment noted. Try running commands now to achieve your goal.");
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
69
|
+
case "trimsession": {
|
|
70
|
+
if (!config.trimSessionEnabled) {
|
|
71
|
+
throw 'The "trimsession" command is not enabled in this environment.';
|
|
72
|
+
}
|
|
73
|
+
const trimSummary = contextManager.trim(cmdArgs);
|
|
74
|
+
await contextManager.append(trimSummary);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
68
77
|
case "endsession": {
|
|
78
|
+
if (!config.endSessionEnabled) {
|
|
79
|
+
throw 'The "trimsession" command is not enabled in this environment.';
|
|
80
|
+
}
|
|
69
81
|
// Don't need to check end line as this is the last command in the context, just read to the end
|
|
70
82
|
const endSessionNotes = utilities.trimChars(cmdArgs, '"');
|
|
71
83
|
if (!endSessionNotes) {
|
|
@@ -105,8 +117,7 @@ export async function processCommand(prompt, consoleInput) {
|
|
|
105
117
|
};
|
|
106
118
|
}
|
|
107
119
|
case "cost": {
|
|
108
|
-
|
|
109
|
-
output.comment(`Total cost so far $${totalCost.toFixed(2)} of $${config.agent.spendLimitDollars} limit`);
|
|
120
|
+
await costTracker.printCosts();
|
|
110
121
|
break;
|
|
111
122
|
}
|
|
112
123
|
case "llmynx": {
|
|
@@ -132,15 +143,18 @@ export async function processCommand(prompt, consoleInput) {
|
|
|
132
143
|
break;
|
|
133
144
|
}
|
|
134
145
|
case "context":
|
|
135
|
-
|
|
146
|
+
output.comment("#####################");
|
|
147
|
+
output.comment(contextManager.printContext());
|
|
148
|
+
output.comment("#####################");
|
|
136
149
|
break;
|
|
150
|
+
case "subagent": {
|
|
151
|
+
const subagentResponse = await subagent.handleCommand(cmdArgs);
|
|
152
|
+
await contextManager.append(subagentResponse);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
137
155
|
default: {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
await output.errorAndLog(`Error detected processing shell command:`);
|
|
141
|
-
processNextLLMpromptBlock = false;
|
|
142
|
-
}
|
|
143
|
-
nextCommandAction = shellResponse.terminate
|
|
156
|
+
const exitApp = await shellCommand.handleCommand(input);
|
|
157
|
+
nextCommandAction = exitApp
|
|
144
158
|
? NextCommandAction.ExitApplication
|
|
145
159
|
: NextCommandAction.Continue;
|
|
146
160
|
}
|
|
@@ -205,8 +219,11 @@ async function splitMultipleInputCommands(nextInput) {
|
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
// If the LLM forgets the quote on the comment, treat it as a single line comment
|
|
222
|
+
// Not something we want to use for multi-line commands like llmail and subagent
|
|
208
223
|
else if (newLinePos > 0 &&
|
|
209
|
-
(nextInput.startsWith("comment ") ||
|
|
224
|
+
(nextInput.startsWith("comment ") ||
|
|
225
|
+
nextInput.startsWith("genimg ") ||
|
|
226
|
+
nextInput.startsWith("trimsession "))) {
|
|
210
227
|
input = nextInput.slice(0, newLinePos);
|
|
211
228
|
nextInput = nextInput.slice(newLinePos).trim();
|
|
212
229
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import * as readline from "readline";
|
|
3
|
-
import * as llmail from "../apps/llmail.js";
|
|
4
|
-
import * as llmynx from "../apps/llmynx.js";
|
|
5
3
|
import * as config from "../config.js";
|
|
4
|
+
import * as llmail from "../features/llmail.js";
|
|
5
|
+
import * as llmynx from "../features/llmynx.js";
|
|
6
|
+
import * as subagent from "../features/subagent.js";
|
|
7
|
+
import * as workspaces from "../features/workspaces.js";
|
|
6
8
|
import * as contextManager from "../llm/contextManager.js";
|
|
7
|
-
import { ContentSource } from "../llm/contextManager.js";
|
|
8
9
|
import * as dreamMaker from "../llm/dreamMaker.js";
|
|
9
|
-
import { LlmRole } from "../llm/llmDtos.js";
|
|
10
|
+
import { ContentSource, LlmRole } from "../llm/llmDtos.js";
|
|
10
11
|
import * as llmService from "../llm/llmService.js";
|
|
12
|
+
import { systemMessage } from "../llm/systemMessage.js";
|
|
11
13
|
import * as inputMode from "../utils/inputMode.js";
|
|
12
14
|
import { InputMode } from "../utils/inputMode.js";
|
|
13
15
|
import * as logService from "../utils/logService.js";
|
|
14
16
|
import * as output from "../utils/output.js";
|
|
17
|
+
import { OutputColor } from "../utils/output.js";
|
|
15
18
|
import * as utilities from "../utils/utilities.js";
|
|
16
19
|
import * as commandHandler from "./commandHandler.js";
|
|
17
20
|
import { NextCommandAction } from "./commandHandler.js";
|
|
@@ -22,7 +25,6 @@ export async function run() {
|
|
|
22
25
|
await output.commentAndLog(`Agent configured to use ${config.agent.shellModel} model`);
|
|
23
26
|
// Show System Message
|
|
24
27
|
await output.commentAndLog("System Message:");
|
|
25
|
-
const systemMessage = contextManager.getSystemMessage();
|
|
26
28
|
output.write(systemMessage);
|
|
27
29
|
await logService.write({
|
|
28
30
|
role: LlmRole.System,
|
|
@@ -31,6 +33,7 @@ export async function run() {
|
|
|
31
33
|
});
|
|
32
34
|
let nextCommandAction = NextCommandAction.Continue;
|
|
33
35
|
let llmErrorCount = 0;
|
|
36
|
+
let nextPromptIndex = 0;
|
|
34
37
|
while (nextCommandAction != NextCommandAction.ExitApplication) {
|
|
35
38
|
inputMode.toggle(InputMode.LLM);
|
|
36
39
|
await output.commentAndLog("Starting Context:");
|
|
@@ -40,30 +43,34 @@ export async function run() {
|
|
|
40
43
|
await contextManager.append(latestDream);
|
|
41
44
|
}
|
|
42
45
|
for (const initialCommand of config.agent.initialCommands) {
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
let prompt = await promptBuilder.getPrompt(0, false);
|
|
47
|
+
prompt = setPromptIndex(prompt, ++nextPromptIndex);
|
|
48
|
+
await contextManager.append(prompt, ContentSource.ConsolePrompt, nextPromptIndex);
|
|
45
49
|
await commandHandler.processCommand(prompt, config.resolveConfigVars(initialCommand));
|
|
46
50
|
}
|
|
47
51
|
inputMode.toggle(InputMode.Debug);
|
|
48
52
|
let pauseSeconds = config.agent.debugPauseSeconds;
|
|
49
53
|
let wakeOnMessage = config.agent.wakeOnMessage;
|
|
50
54
|
while (nextCommandAction == NextCommandAction.Continue) {
|
|
51
|
-
|
|
55
|
+
let prompt = await promptBuilder.getPrompt(pauseSeconds, wakeOnMessage);
|
|
52
56
|
let consoleInput = "";
|
|
53
57
|
// Debug command prompt
|
|
54
58
|
if (inputMode.current === InputMode.Debug) {
|
|
59
|
+
subagent.unreadContextSummary();
|
|
55
60
|
consoleInput = await promptBuilder.getInput(`${prompt}`, pauseSeconds, wakeOnMessage);
|
|
56
61
|
}
|
|
57
62
|
// LLM command prompt
|
|
58
63
|
else if (inputMode.current === InputMode.LLM) {
|
|
64
|
+
prompt = setPromptIndex(prompt, ++nextPromptIndex);
|
|
59
65
|
const workingMsg = prompt +
|
|
60
|
-
chalk[
|
|
66
|
+
chalk[OutputColor.loading](`LLM (${config.agent.shellModel}) Working...`);
|
|
61
67
|
try {
|
|
62
68
|
await checkNewMailNotification();
|
|
63
69
|
await checkContextLimitWarning();
|
|
64
|
-
await
|
|
70
|
+
await workspaces.displayActive();
|
|
71
|
+
await contextManager.append(prompt, ContentSource.ConsolePrompt, nextPromptIndex);
|
|
65
72
|
process.stdout.write(workingMsg);
|
|
66
|
-
consoleInput = await llmService.query(config.agent.shellModel, contextManager.
|
|
73
|
+
consoleInput = await llmService.query(config.agent.shellModel, systemMessage, contextManager.getCombinedMessages(), "console");
|
|
67
74
|
clearPromptMessage(workingMsg);
|
|
68
75
|
}
|
|
69
76
|
catch (e) {
|
|
@@ -101,6 +108,7 @@ export async function run() {
|
|
|
101
108
|
llmynx.clear();
|
|
102
109
|
contextManager.clear();
|
|
103
110
|
nextCommandAction = NextCommandAction.Continue;
|
|
111
|
+
nextPromptIndex = 0;
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
}
|
|
@@ -178,7 +186,7 @@ async function checkNewMailNotification() {
|
|
|
178
186
|
for (const unreadThread of unreadThreads) {
|
|
179
187
|
await llmail.markAsRead(unreadThread.threadId);
|
|
180
188
|
}
|
|
181
|
-
mailBlackoutCountdown = config.mailBlackoutCycles;
|
|
189
|
+
mailBlackoutCountdown = config.agent.mailBlackoutCycles || 0;
|
|
182
190
|
}
|
|
183
191
|
else if (llmail.simpleMode) {
|
|
184
192
|
await contextManager.append(`You have new mail, but not enough context to read them.\n` +
|
|
@@ -196,11 +204,34 @@ async function checkContextLimitWarning() {
|
|
|
196
204
|
const tokenCount = contextManager.getTokenCount();
|
|
197
205
|
const tokenMax = config.agent.tokenMax;
|
|
198
206
|
if (tokenCount > tokenMax) {
|
|
199
|
-
|
|
200
|
-
|
|
207
|
+
let tokenNote = "";
|
|
208
|
+
if (config.endSessionEnabled) {
|
|
209
|
+
tokenNote += `\nUse 'endsession <note>' to clear the console and reset the session.
|
|
201
210
|
The note should help you find your bearings in the next session.
|
|
202
|
-
The note should contain your next goal, and important things should you remember
|
|
203
|
-
|
|
211
|
+
The note should contain your next goal, and important things should you remember.`;
|
|
212
|
+
}
|
|
213
|
+
if (config.trimSessionEnabled) {
|
|
214
|
+
tokenNote += `\nUse 'trimsession' to reduce the size of the session.
|
|
215
|
+
Use comments to remember important things from trimmed prompts.`;
|
|
216
|
+
}
|
|
217
|
+
await contextManager.append(`The token limit for this session has been exceeded.${tokenNote}`, ContentSource.Console);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Insert prompt index [Index: 1] before the $.
|
|
221
|
+
* Insert at the end of the prompt so that 'prompt splitting' still works in the command handler
|
|
222
|
+
*/
|
|
223
|
+
function setPromptIndex(prompt, index) {
|
|
224
|
+
if (!config.trimSessionEnabled) {
|
|
225
|
+
return prompt;
|
|
226
|
+
}
|
|
227
|
+
let newPrompt = prompt;
|
|
228
|
+
const endPromptPos = prompt.lastIndexOf("$");
|
|
229
|
+
if (endPromptPos != -1) {
|
|
230
|
+
newPrompt =
|
|
231
|
+
prompt.slice(0, endPromptPos) +
|
|
232
|
+
` [Index: ${index}]` +
|
|
233
|
+
prompt.slice(endPromptPos);
|
|
204
234
|
}
|
|
235
|
+
return newPrompt;
|
|
205
236
|
}
|
|
206
237
|
//# sourceMappingURL=commandLoop.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import * as events from "events";
|
|
3
3
|
import * as readline from "readline";
|
|
4
|
-
import * as llmail from "../apps/llmail.js";
|
|
5
4
|
import * as config from "../config.js";
|
|
5
|
+
import * as llmail from "../features/llmail.js";
|
|
6
6
|
import * as contextManager from "../llm/contextManager.js";
|
|
7
7
|
import * as inputMode from "../utils/inputMode.js";
|
|
8
8
|
import { InputMode } from "../utils/inputMode.js";
|
|
@@ -17,6 +17,7 @@ const _outputEmitter = new events.EventEmitter();
|
|
|
17
17
|
const _originalWrite = process.stdout.write.bind(process.stdout);
|
|
18
18
|
process.stdout.write = (...args) => {
|
|
19
19
|
_outputEmitter.emit(_writeEventName, false, ...args);
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
21
|
return _originalWrite.apply(process.stdout, args);
|
|
21
22
|
};
|
|
22
23
|
const _readlineInterface = readline.createInterface({
|
|
@@ -6,52 +6,45 @@ import * as utilities from "../utils/utilities.js";
|
|
|
6
6
|
import * as shellWrapper from "./shellWrapper.js";
|
|
7
7
|
export async function handleCommand(input) {
|
|
8
8
|
const cmdParams = input.split(" ");
|
|
9
|
-
const response = {
|
|
10
|
-
hasErrors: true,
|
|
11
|
-
};
|
|
12
9
|
// Route user to context friendly edit commands that can read/write the entire file in one go
|
|
13
10
|
// Having EOF in quotes is important as it prevents the shell from replacing $variables with bash values
|
|
14
11
|
if (["nano", "vi", "vim"].includes(cmdParams[0])) {
|
|
15
|
-
|
|
16
|
-
return response;
|
|
12
|
+
throw `${cmdParams[0]} not supported. Use \`cat\` to read a file and \`cat > filename << 'EOF'\` to write a file`;
|
|
17
13
|
}
|
|
18
14
|
if (cmdParams[0] == "lynx" && cmdParams[1] != "--dump") {
|
|
19
|
-
|
|
20
|
-
return response;
|
|
15
|
+
throw `Interactive mode with lynx is not supported. Use --dump with lynx to view a website`;
|
|
21
16
|
}
|
|
22
17
|
if (cmdParams[0] == "exit") {
|
|
23
18
|
if (inputMode.current == InputMode.LLM) {
|
|
24
|
-
|
|
19
|
+
throw "Use 'endsession' to end the session and clear the console log.";
|
|
25
20
|
}
|
|
21
|
+
// Only the debug user is allowed to exit the shell
|
|
26
22
|
else if (inputMode.current == InputMode.Debug) {
|
|
27
23
|
await shellWrapper.terminate();
|
|
28
|
-
|
|
24
|
+
return true;
|
|
29
25
|
}
|
|
30
|
-
return response;
|
|
31
26
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (text.endsWith(": command not found")) {
|
|
51
|
-
await contextManager.append("Please enter a valid Linux or NAISYS command after the prompt. Use the 'comment' command for thoughts.");
|
|
52
|
-
}
|
|
27
|
+
let response = await shellWrapper.executeCommand(input);
|
|
28
|
+
let outputLimitExceeded = false;
|
|
29
|
+
const tokenCount = utilities.getTokenCount(response);
|
|
30
|
+
// Prevent too much output from blowing up the context
|
|
31
|
+
if (tokenCount > config.shellCommand.outputTokenMax) {
|
|
32
|
+
outputLimitExceeded = true;
|
|
33
|
+
const trimLength = (response.length * config.shellCommand.outputTokenMax) / tokenCount;
|
|
34
|
+
response =
|
|
35
|
+
response.slice(0, trimLength / 2) +
|
|
36
|
+
"\n\n...\n\n" +
|
|
37
|
+
response.slice(-trimLength / 2);
|
|
38
|
+
}
|
|
39
|
+
if (outputLimitExceeded) {
|
|
40
|
+
response += `\nThe shell command generated too much output (${tokenCount} tokens). Only 2,000 tokens worth are shown above.`;
|
|
41
|
+
}
|
|
42
|
+
if (response.endsWith(": command not found")) {
|
|
43
|
+
response +=
|
|
44
|
+
"Please enter a valid Linux or NAISYS command after the prompt. Use the 'comment' command for thoughts.";
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
// todo move this into the command handler to remove the context manager dependency
|
|
47
|
+
await contextManager.append(response);
|
|
48
|
+
return false;
|
|
56
49
|
}
|
|
57
50
|
//# sourceMappingURL=shellCommand.js.map
|
|
@@ -3,7 +3,7 @@ import * as fs from "fs";
|
|
|
3
3
|
import * as os from "os";
|
|
4
4
|
import * as config from "../config.js";
|
|
5
5
|
import * as output from "../utils/output.js";
|
|
6
|
-
import {
|
|
6
|
+
import { NaisysPath } from "../utils/pathService.js";
|
|
7
7
|
var ShellEvent;
|
|
8
8
|
(function (ShellEvent) {
|
|
9
9
|
ShellEvent["Ouptput"] = "stdout";
|
|
@@ -11,39 +11,43 @@ var ShellEvent;
|
|
|
11
11
|
ShellEvent["Exit"] = "exit";
|
|
12
12
|
})(ShellEvent || (ShellEvent = {}));
|
|
13
13
|
let _process;
|
|
14
|
-
|
|
14
|
+
let _currentProcessId;
|
|
15
15
|
let _commandOutput = "";
|
|
16
|
-
let _hasErrors = false;
|
|
17
16
|
let _currentPath;
|
|
18
17
|
let _resolveCurrentCommand;
|
|
19
18
|
let _currentCommandTimeout;
|
|
19
|
+
let _startTime;
|
|
20
|
+
/** How we know the command has completed when running the command inside a shell like bash or wsl */
|
|
20
21
|
const _commandDelimiter = "__COMMAND_END_X7YUTT__";
|
|
21
22
|
async function ensureOpen() {
|
|
22
23
|
if (_process) {
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
|
-
//_log = "";
|
|
26
26
|
resetCommand();
|
|
27
27
|
const spawnProcess = os.platform() === "win32" ? "wsl" : "bash";
|
|
28
28
|
_process = spawn(spawnProcess, [], { stdio: "pipe" });
|
|
29
|
+
const pid = _process.pid;
|
|
30
|
+
if (!pid) {
|
|
31
|
+
throw "Shell process failed to start";
|
|
32
|
+
}
|
|
33
|
+
_currentProcessId = pid;
|
|
29
34
|
_process.stdout.on("data", (data) => {
|
|
30
|
-
processOutput(data.toString(), ShellEvent.Ouptput);
|
|
35
|
+
processOutput(data.toString(), ShellEvent.Ouptput, pid);
|
|
31
36
|
});
|
|
32
37
|
_process.stderr.on("data", (data) => {
|
|
33
|
-
processOutput(data.toString(), ShellEvent.Error);
|
|
38
|
+
processOutput(data.toString(), ShellEvent.Error, pid);
|
|
34
39
|
});
|
|
35
40
|
_process.on("close", (code) => {
|
|
36
|
-
processOutput(`${code}`, ShellEvent.Exit);
|
|
37
|
-
_process = undefined;
|
|
41
|
+
processOutput(`${code}`, ShellEvent.Exit, pid);
|
|
38
42
|
});
|
|
39
43
|
// Init users home dir on first run, on shell crash/rerun go back to the current path
|
|
40
44
|
if (!_currentPath) {
|
|
41
|
-
output.comment("NEW SHELL OPENED. PID: " +
|
|
45
|
+
output.comment("NEW SHELL OPENED. PID: " + pid);
|
|
42
46
|
errorIfNotEmpty(await executeCommand(`mkdir -p ${config.naisysFolder}/home/` + config.agent.username));
|
|
43
47
|
errorIfNotEmpty(await executeCommand(`cd ${config.naisysFolder}/home/` + config.agent.username));
|
|
44
48
|
}
|
|
45
49
|
else {
|
|
46
|
-
output.comment("SHELL RESTORED. PID: " +
|
|
50
|
+
output.comment("SHELL RESTORED. PID: " + pid);
|
|
47
51
|
errorIfNotEmpty(await executeCommand("cd " + _currentPath));
|
|
48
52
|
}
|
|
49
53
|
// Stop running commands if one fails
|
|
@@ -53,102 +57,92 @@ async function ensureOpen() {
|
|
|
53
57
|
}
|
|
54
58
|
/** Basically don't show anything in the console unless there is an error */
|
|
55
59
|
function errorIfNotEmpty(response) {
|
|
56
|
-
if (response
|
|
57
|
-
output.error(response
|
|
60
|
+
if (response) {
|
|
61
|
+
output.error(response);
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
function processOutput(dataStr, eventType) {
|
|
64
|
+
function processOutput(dataStr, eventType, pid) {
|
|
65
|
+
if (pid != _currentProcessId) {
|
|
66
|
+
output.comment(`Ignoring '${eventType}' from old shell process ${pid}: ` + dataStr);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
61
69
|
if (!_resolveCurrentCommand) {
|
|
62
|
-
output.comment(eventType
|
|
70
|
+
output.comment(`Ignoring '${eventType}' from process ${pid} with no resolve handler: ` +
|
|
71
|
+
dataStr);
|
|
63
72
|
return;
|
|
64
73
|
}
|
|
65
74
|
if (eventType === ShellEvent.Exit) {
|
|
66
75
|
output.error("SHELL EXITED. PID: " + _process?.pid + " CODE: " + dataStr);
|
|
76
|
+
const elapsedSeconds = _startTime
|
|
77
|
+
? Math.round((new Date().getTime() - _startTime.getTime()) / 1000)
|
|
78
|
+
: -1;
|
|
79
|
+
const outputWithError = _commandOutput.trim() +
|
|
80
|
+
`\nNAISYS: Command hit time out limit after ${elapsedSeconds} seconds. If possible figure out how to run the command faster or break it up into smaller parts.`;
|
|
81
|
+
resetProcess();
|
|
82
|
+
_resolveCurrentCommand(outputWithError);
|
|
83
|
+
return;
|
|
67
84
|
}
|
|
68
85
|
else {
|
|
69
|
-
//
|
|
86
|
+
// Extend the timeout of the current command
|
|
87
|
+
setOrExtendShellTimeout();
|
|
70
88
|
_commandOutput += dataStr;
|
|
71
89
|
}
|
|
72
|
-
if (eventType === ShellEvent.Error) {
|
|
73
|
-
_hasErrors = true;
|
|
74
|
-
//output += "stderr: ";
|
|
75
|
-
// parse out the line number from '-bash: line 999: '
|
|
76
|
-
/*if (dataStr.startsWith("-bash: line ")) {
|
|
77
|
-
output.error(dataStr);
|
|
78
|
-
|
|
79
|
-
const lineNum = dataStr.slice(11, dataStr.indexOf(": ", 11));
|
|
80
|
-
output.error(`Detected error on line ${lineNum} of output`);
|
|
81
|
-
|
|
82
|
-
// display the same line of _output
|
|
83
|
-
const logLines = _log.split("\n");
|
|
84
|
-
const lineIndex = parseInt(lineNum) - 1;
|
|
85
|
-
if (logLines.length > lineIndex) {
|
|
86
|
-
output.error(`Line ${lineIndex} in log: ` + logLines[lineIndex]);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// output all lines for debugging
|
|
90
|
-
for (let i = 0; i < logLines.length; i++) {
|
|
91
|
-
// if withing 10 lines of the error, show the line
|
|
92
|
-
//if (Math.abs(i - lineIndex) < 10) {
|
|
93
|
-
const lineStr = logLines[i].replace(/\n/g, "");
|
|
94
|
-
output.error(`${i}: ${lineStr}`);
|
|
95
|
-
//}
|
|
96
|
-
}
|
|
97
|
-
}*/
|
|
98
|
-
}
|
|
99
90
|
const delimiterIndex = _commandOutput.indexOf(_commandDelimiter);
|
|
100
|
-
if (delimiterIndex != -1
|
|
91
|
+
if (delimiterIndex != -1) {
|
|
101
92
|
// trim everything after delimiter
|
|
102
93
|
_commandOutput = _commandOutput.slice(0, delimiterIndex);
|
|
103
|
-
const response =
|
|
104
|
-
value: _commandOutput.trim(),
|
|
105
|
-
hasErrors: _hasErrors,
|
|
106
|
-
};
|
|
94
|
+
const response = _commandOutput.trim();
|
|
107
95
|
resetCommand();
|
|
108
96
|
_resolveCurrentCommand(response);
|
|
109
97
|
}
|
|
110
98
|
}
|
|
111
99
|
export async function executeCommand(command) {
|
|
112
|
-
/*if (command == "shelllog") {
|
|
113
|
-
_log.split("\n").forEach((line, i) => {
|
|
114
|
-
output.comment(`${i}. ${line}`);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return <CommandResponse>{
|
|
118
|
-
value: "",
|
|
119
|
-
hasErrors: false,
|
|
120
|
-
};
|
|
121
|
-
}*/
|
|
122
100
|
await ensureOpen();
|
|
123
101
|
if (_currentPath && command.trim().split("\n").length > 1) {
|
|
124
|
-
command = await
|
|
102
|
+
command = await putMultilineCommandInAScript(command);
|
|
125
103
|
}
|
|
126
|
-
return new Promise((resolve) => {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
127
105
|
_resolveCurrentCommand = resolve;
|
|
128
106
|
const commandWithDelimiter = `${command.trim()}\necho "${_commandDelimiter} LINE:\${LINENO}"\n`;
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
if (!_process) {
|
|
108
|
+
reject("Shell process is not open");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
_process.stdin.write(commandWithDelimiter);
|
|
112
|
+
_startTime = new Date();
|
|
131
113
|
// If no response, kill and reset the shell, often hanging on some unescaped input
|
|
132
|
-
|
|
114
|
+
setOrExtendShellTimeout();
|
|
133
115
|
});
|
|
134
116
|
}
|
|
135
|
-
function
|
|
136
|
-
if
|
|
117
|
+
function setOrExtendShellTimeout() {
|
|
118
|
+
// Don't extend if we've been waiting longer than the max timeout seconds
|
|
119
|
+
const timeWaiting = new Date().getTime() - (_startTime?.getTime() || 0);
|
|
120
|
+
if (!_process?.pid ||
|
|
121
|
+
timeWaiting > config.shellCommand.maxTimeoutSeconds * 1000) {
|
|
137
122
|
return;
|
|
138
123
|
}
|
|
139
|
-
_process
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
124
|
+
// Define the pid for use in the timeout closure, as _process.pid may change
|
|
125
|
+
const pid = _process.pid;
|
|
126
|
+
clearTimeout(_currentCommandTimeout);
|
|
127
|
+
_currentCommandTimeout = setTimeout(() => {
|
|
128
|
+
resetShell(pid);
|
|
129
|
+
}, config.shellCommand.timeoutSeconds * 1000);
|
|
130
|
+
}
|
|
131
|
+
function resetShell(pid) {
|
|
132
|
+
if (!_process || _process.pid != pid) {
|
|
133
|
+
output.comment("Ignoring timeout for old shell process " + pid);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// There is still an issue here when running on linux where if a command like 'ping' is running
|
|
137
|
+
// then kill() won't actually kill the 'bash' process hosting the ping, it will just hang here indefinitely
|
|
138
|
+
// A not fail proof workaround is to tell the LLM to prefix long running commands with 'timeout 10s' or similar
|
|
139
|
+
const killResponse = _process.kill();
|
|
140
|
+
output.error(`KILL SIGNAL SENT TO PID: ${_process.pid}, RESPONSE: ${killResponse ? "SUCCESS" : "FAILED"}`);
|
|
141
|
+
// Should trigger the process close event from here
|
|
148
142
|
}
|
|
149
143
|
export async function getCurrentPath() {
|
|
150
144
|
await ensureOpen();
|
|
151
|
-
_currentPath =
|
|
145
|
+
_currentPath = await executeCommand("pwd");
|
|
152
146
|
return _currentPath;
|
|
153
147
|
}
|
|
154
148
|
export async function terminate() {
|
|
@@ -158,7 +152,7 @@ export async function terminate() {
|
|
|
158
152
|
}
|
|
159
153
|
function resetCommand() {
|
|
160
154
|
_commandOutput = "";
|
|
161
|
-
|
|
155
|
+
_startTime = undefined;
|
|
162
156
|
clearTimeout(_currentCommandTimeout);
|
|
163
157
|
}
|
|
164
158
|
function resetProcess() {
|
|
@@ -168,18 +162,18 @@ function resetProcess() {
|
|
|
168
162
|
}
|
|
169
163
|
/** Wraps multi line commands in a script to make it easier to diagnose the source of errors based on line number
|
|
170
164
|
* May also help with common escaping errors */
|
|
171
|
-
function
|
|
172
|
-
const scriptPath = `${config.naisysFolder}/home/${config.agent.username}/.command.tmp.sh
|
|
165
|
+
function putMultilineCommandInAScript(command) {
|
|
166
|
+
const scriptPath = new NaisysPath(`${config.naisysFolder}/home/${config.agent.username}/.command.tmp.sh`);
|
|
173
167
|
// set -e causes the script to exit on the first error
|
|
174
168
|
const scriptContent = `#!/bin/bash
|
|
175
169
|
set -e
|
|
176
170
|
cd ${_currentPath}
|
|
177
171
|
${command.trim()}`;
|
|
178
|
-
// create/
|
|
179
|
-
fs.writeFileSync(
|
|
172
|
+
// create/write file
|
|
173
|
+
fs.writeFileSync(scriptPath.toHostPath(), scriptContent);
|
|
180
174
|
// `Path` is set to the ./bin folder because custom NAISYS commands that follow shell commands will be handled by the shell, which will fail
|
|
181
175
|
// so we need to remind the LLM that 'naisys commands cannot be used with other commands on the same prompt'
|
|
182
176
|
// `source` will run the script in the current shell, so any change directories in the script will persist in the current shell
|
|
183
|
-
return `PATH=${config.binPath}:$PATH source ${scriptPath}`;
|
|
177
|
+
return `PATH=${config.binPath}:$PATH source ${scriptPath.getNaisysPath()}`;
|
|
184
178
|
}
|
|
185
179
|
//# sourceMappingURL=shellWrapper.js.map
|