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 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: gemini-pro
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
- - If you want to use NAISYS for a website
194
- - Install a local web server, for example [XAMPP](https://www.apachefriends.org/) on Windows
195
- - Start the server and put the URL in the `.env` file
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
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ echo "'trimsession' cannot be used with other commands on the same prompt."
@@ -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
- const totalCost = await costTracker.getTotalCosts();
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
- contextManager.printContext();
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 shellResponse = await shellCommand.handleCommand(input);
139
- if (shellResponse.hasErrors && nextInput) {
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 ") || nextInput.startsWith("genimg "))) {
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
- const prompt = await promptBuilder.getPrompt(0, false);
44
- await contextManager.append(prompt, ContentSource.ConsolePrompt);
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
- const prompt = await promptBuilder.getPrompt(pauseSeconds, wakeOnMessage);
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[output.OutputColor.loading](`LLM (${config.agent.shellModel}) Working...`);
66
+ chalk[OutputColor.loading](`LLM (${config.agent.shellModel}) Working...`);
61
67
  try {
62
68
  await checkNewMailNotification();
63
69
  await checkContextLimitWarning();
64
- await contextManager.append(prompt, ContentSource.ConsolePrompt);
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.getSystemMessage(), contextManager.messages, "console");
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
- await contextManager.append(`The token limit for this session has been exceeded.
200
- Use \`endsession <note>\` to clear the console and reset the session.
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
- Try to keep the note around 400 tokens.`, ContentSource.Console);
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
- await contextManager.append(`${cmdParams[0]} not supported. Use \`cat\` to read a file and \`cat > filename << 'EOF'\` to write a file`);
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
- await contextManager.append(`Interactive mode with lynx is not supported. Use --dump with lynx to view a website`);
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
- await contextManager.append("Use 'endsession' to end the session and clear the console log.");
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
- response.terminate = true;
24
+ return true;
29
25
  }
30
- return response;
31
26
  }
32
- const output = await shellWrapper.executeCommand(input);
33
- if (output.value) {
34
- let text = output.value;
35
- let outputLimitExceeded = false;
36
- const tokenCount = utilities.getTokenCount(text);
37
- // Prevent too much output from blowing up the context
38
- if (tokenCount > config.shellOutputTokenMax) {
39
- outputLimitExceeded = true;
40
- const trimLength = (text.length * config.shellOutputTokenMax) / tokenCount;
41
- text =
42
- text.slice(0, trimLength / 2) +
43
- "\n\n...\n\n" +
44
- text.slice(-trimLength / 2);
45
- }
46
- await contextManager.append(text);
47
- if (outputLimitExceeded) {
48
- await contextManager.append(`\nThe shell command generated too much output (${tokenCount} tokens). Only 2,000 tokens worth are shown above.`);
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
- response.hasErrors = output.hasErrors;
55
- return response;
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 { unixToHostPath } from "../utils/utilities.js";
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
- //let _log = "";
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: " + _process.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: " + _process.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.value) {
57
- output.error(response.value);
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 + " without handler: " + dataStr);
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
- //_log += "OUTPUT: " + dataStr;
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 || eventType === ShellEvent.Exit) {
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 runCommandFromScript(command);
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
- //_log += "INPUT: " + commandWithDelimiter;
130
- _process?.stdin.write(commandWithDelimiter);
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
- _currentCommandTimeout = setTimeout(resetShell, config.shellCommmandTimeoutSeconds * 1000);
114
+ setOrExtendShellTimeout();
133
115
  });
134
116
  }
135
- function resetShell() {
136
- if (!_resolveCurrentCommand) {
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?.kill();
140
- output.error("SHELL TIMEMOUT/KILLED. PID: " + _process?.pid);
141
- const outputWithError = _commandOutput.trim() +
142
- `\nError: Command timed out after ${config.shellCommmandTimeoutSeconds} seconds.`;
143
- resetProcess();
144
- _resolveCurrentCommand({
145
- value: outputWithError,
146
- hasErrors: true,
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 = (await executeCommand("pwd")).value;
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
- _hasErrors = false;
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 runCommandFromScript(command) {
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/writewrite file
179
- fs.writeFileSync(unixToHostPath(scriptPath), scriptContent);
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