chainlesschain 0.40.3 → 0.41.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
@@ -176,7 +176,7 @@ chainlesschain llm switch <name> # Switch active provider
176
176
 
177
177
  ### `chainlesschain agent` (alias: `a`)
178
178
 
179
- Start an agentic AI session — the AI can read/write files, run shell commands, search the codebase, and invoke 138 built-in skills.
179
+ Start an agentic AI session — the AI can read/write files, run shell commands, search the codebase, execute code (Python/Node.js/Bash with auto pip-install), and invoke 138 built-in skills.
180
180
 
181
181
  ```bash
182
182
  chainlesschain agent # Default: Ollama qwen2.5:7b
@@ -908,7 +908,7 @@ Configuration is stored at `~/.chainlesschain/config.json`. The CLI creates and
908
908
  ```bash
909
909
  cd packages/cli
910
910
  npm install
911
- npm test # Run all tests (2432 tests across 109 files)
911
+ npm test # Run all tests (2503 tests across 113 files)
912
912
  npm run test:unit # Unit tests only
913
913
  npm run test:integration # Integration tests
914
914
  npm run test:e2e # End-to-end tests
@@ -926,7 +926,7 @@ npm run test:e2e # End-to-end tests
926
926
  | Core packages (external) | — | 118 | All passing |
927
927
  | Unit — WS sessions | 9 | 156 | All passing |
928
928
  | Integration — WS session | 1 | 12 | All passing |
929
- | **CLI Total** | **109** | **2432** | **All passing** |
929
+ | **CLI Total** | **113** | **2503** | **All passing** |
930
930
 
931
931
  ## License
932
932
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.40.3",
3
+ "version": "0.41.0",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * serve command — start a WebSocket server for remote CLI access
3
- * chainlesschain serve [--port] [--host] [--token] [--max-connections] [--timeout] [--allow-remote]
3
+ * chainlesschain serve [--port] [--host] [--token] [--max-connections] [--timeout] [--allow-remote] [--project]
4
4
  */
5
5
 
6
6
  import chalk from "chalk";
7
7
  import { logger } from "../lib/logger.js";
8
8
  import { ChainlessChainWSServer } from "../lib/ws-server.js";
9
+ import { WSSessionManager } from "../lib/ws-session-manager.js";
10
+ import { bootstrap } from "../runtime/bootstrap.js";
9
11
 
10
12
  export function registerServeCommand(program) {
11
13
  program
@@ -27,6 +29,7 @@ export function registerServeCommand(program) {
27
29
  "--allow-remote",
28
30
  "Allow non-localhost connections (requires --token)",
29
31
  )
32
+ .option("--project <path>", "Default project root for sessions")
30
33
  .action(async (opts) => {
31
34
  const port = parseInt(opts.port, 10);
32
35
  const maxConnections = parseInt(opts.maxConnections, 10);
@@ -47,12 +50,32 @@ export function registerServeCommand(program) {
47
50
  host = "0.0.0.0";
48
51
  }
49
52
 
53
+ // Bootstrap headless runtime for DB access
54
+ let db = null;
55
+ try {
56
+ const ctx = await bootstrap({ skipDb: false });
57
+ db = ctx.db?.getDb?.() || null;
58
+ } catch (_err) {
59
+ logger.log(
60
+ chalk.yellow(
61
+ " Warning: Database not available, sessions will be in-memory only",
62
+ ),
63
+ );
64
+ }
65
+
66
+ // Create session manager
67
+ const sessionManager = new WSSessionManager({
68
+ db,
69
+ defaultProjectRoot: opts.project || process.cwd(),
70
+ });
71
+
50
72
  const server = new ChainlessChainWSServer({
51
73
  port,
52
74
  host,
53
75
  token: opts.token || null,
54
76
  maxConnections,
55
77
  timeout,
78
+ sessionManager,
56
79
  });
57
80
 
58
81
  // Event logging
@@ -76,6 +99,14 @@ export function registerServeCommand(program) {
76
99
  logger.log(color(` < [${id}] exit ${exitCode}`));
77
100
  });
78
101
 
102
+ server.on("session:create", ({ sessionId, type }) => {
103
+ logger.log(chalk.green(` + Session created: ${sessionId} (${type})`));
104
+ });
105
+
106
+ server.on("session:close", ({ sessionId }) => {
107
+ logger.log(chalk.yellow(` - Session closed: ${sessionId}`));
108
+ });
109
+
79
110
  // Graceful shutdown
80
111
  const shutdown = async () => {
81
112
  logger.log("\n" + chalk.yellow("Shutting down WebSocket server..."));
@@ -96,6 +127,8 @@ export function registerServeCommand(program) {
96
127
  logger.log(
97
128
  ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
98
129
  );
130
+ logger.log(` Sessions: ${chalk.green("enabled")}`);
131
+ logger.log(` Project: ${opts.project || process.cwd()}`);
99
132
  logger.log(` Max conn: ${maxConnections}`);
100
133
  logger.log(` Timeout: ${timeout}ms`);
101
134
  logger.log("");
@@ -21,6 +21,7 @@ import os from "os";
21
21
  import { getPlanModeManager } from "./plan-mode.js";
22
22
  import { CLISkillLoader } from "./skill-loader.js";
23
23
  import { executeHooks, HookEvents } from "./hook-manager.js";
24
+ import { detectPython } from "./cli-anything-bridge.js";
24
25
 
25
26
  // ─── Tool definitions ────────────────────────────────────────────────────
26
27
 
@@ -186,7 +187,7 @@ export const AGENT_TOOLS = [
186
187
  function: {
187
188
  name: "run_code",
188
189
  description:
189
- "Write and execute code in Python, Node.js, or Bash. Use this when the user needs data processing, calculations, file batch operations, API calls, or any task best solved with a script. The code is saved to a temp file and executed.",
190
+ "Write and execute code in Python, Node.js, or Bash. Use this when the user needs data processing, calculations, file batch operations, API calls, or any task best solved with a script. Scripts are saved for reference. Missing Python packages are auto-installed.",
190
191
  parameters: {
191
192
  type: "object",
192
193
  properties: {
@@ -200,6 +201,11 @@ export const AGENT_TOOLS = [
200
201
  type: "number",
201
202
  description: "Execution timeout in seconds (default: 60, max: 300)",
202
203
  },
204
+ persist: {
205
+ type: "boolean",
206
+ description:
207
+ "If true (default), save script in .chainlesschain/agent-scripts/. If false, use temp file and clean up.",
208
+ },
203
209
  },
204
210
  required: ["language", "code"],
205
211
  },
@@ -211,9 +217,91 @@ export const AGENT_TOOLS = [
211
217
 
212
218
  const _defaultSkillLoader = new CLISkillLoader();
213
219
 
220
+ // ─── Cached environment detection ────────────────────────────────────────
221
+
222
+ let _cachedPython = null;
223
+ let _cachedEnvInfo = null;
224
+
225
+ /**
226
+ * Get cached Python interpreter info (reuses cli-anything-bridge detection).
227
+ * @returns {{ found: boolean, command?: string, version?: string }}
228
+ */
229
+ export function getCachedPython() {
230
+ if (!_cachedPython) {
231
+ _cachedPython = detectPython();
232
+ }
233
+ return _cachedPython;
234
+ }
235
+
236
+ /**
237
+ * Gather environment info (cached once per process).
238
+ * @returns {{ os: string, arch: string, python: string|null, pip: boolean, node: string|null, git: boolean }}
239
+ */
240
+ export function getEnvironmentInfo() {
241
+ if (_cachedEnvInfo) return _cachedEnvInfo;
242
+
243
+ const py = getCachedPython();
244
+
245
+ let pipAvailable = false;
246
+ if (py.found) {
247
+ try {
248
+ execSync(`${py.command} -m pip --version`, {
249
+ encoding: "utf-8",
250
+ timeout: 10000,
251
+ stdio: ["pipe", "pipe", "pipe"],
252
+ });
253
+ pipAvailable = true;
254
+ } catch {
255
+ // pip not available
256
+ }
257
+ }
258
+
259
+ let nodeVersion = null;
260
+ try {
261
+ nodeVersion = execSync("node --version", {
262
+ encoding: "utf-8",
263
+ timeout: 5000,
264
+ }).trim();
265
+ } catch {
266
+ // Node not available (unlikely since we're running in Node)
267
+ }
268
+
269
+ let gitAvailable = false;
270
+ try {
271
+ execSync("git --version", {
272
+ encoding: "utf-8",
273
+ timeout: 5000,
274
+ stdio: ["pipe", "pipe", "pipe"],
275
+ });
276
+ gitAvailable = true;
277
+ } catch {
278
+ // git not available
279
+ }
280
+
281
+ _cachedEnvInfo = {
282
+ os: process.platform,
283
+ arch: process.arch,
284
+ python: py.found ? `${py.command} (${py.version})` : null,
285
+ pip: pipAvailable,
286
+ node: nodeVersion,
287
+ git: gitAvailable,
288
+ };
289
+ return _cachedEnvInfo;
290
+ }
291
+
214
292
  // ─── System prompt ────────────────────────────────────────────────────────
215
293
 
216
294
  export function getBaseSystemPrompt(cwd) {
295
+ const env = getEnvironmentInfo();
296
+ const envLines = [
297
+ `OS: ${env.os} (${env.arch})`,
298
+ env.python
299
+ ? `Python: ${env.python}${env.pip ? " + pip" : ""}`
300
+ : "Python: not found",
301
+ env.node ? `Node.js: ${env.node}` : "Node.js: not found",
302
+ `Git: ${env.git ? "available" : "not found"}`,
303
+ ];
304
+
217
305
  return `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
218
306
 
219
307
  You have access to tools that let you read files, write files, edit files, run shell commands, and search the codebase. When the user asks you to do something, USE THE TOOLS to actually do it — don't just describe what should be done.
@@ -230,11 +318,16 @@ Key behaviors:
230
318
  When the user's problem involves data processing, calculations, file operations, text parsing, API calls, web scraping, or any task that can be solved programmatically:
231
319
  - Proactively write and execute code using run_code tool
232
320
  - Choose the best language: Python for data/math/scraping, Node.js for JSON/API, Bash for system tasks
321
+ - Missing Python packages are auto-installed via pip when import errors are detected
322
+ - Scripts are persisted in .chainlesschain/agent-scripts/ for reference
233
323
  - Show the results and explain them clearly
234
324
  - If the first attempt fails, debug and retry with a different approach
235
325
 
236
326
  You are not just a chatbot — you are a capable coding agent. Think step by step, write code when needed, and deliver real results.
237
327
 
328
+ ## Environment
329
+ ${envLines.join("\n")}
330
+
238
331
  Current working directory: ${cwd || process.cwd()}`;
239
332
  }
240
333
 
@@ -388,66 +481,7 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
388
481
  }
389
482
 
390
483
  case "run_code": {
391
- const lang = args.language;
392
- const code = args.code;
393
- const timeoutSec = Math.min(Math.max(args.timeout || 60, 1), 300);
394
-
395
- const extMap = { python: ".py", node: ".js", bash: ".sh" };
396
- const ext = extMap[lang];
397
- if (!ext) {
398
- return {
399
- error: `Unsupported language: ${lang}. Use python, node, or bash.`,
400
- };
401
- }
402
-
403
- const tmpFile = path.join(os.tmpdir(), `cc-agent-${Date.now()}${ext}`);
404
-
405
- try {
406
- fs.writeFileSync(tmpFile, code, "utf8");
407
-
408
- let interpreter;
409
- if (lang === "python") {
410
- try {
411
- execSync("python3 --version", { encoding: "utf8", timeout: 5000 });
412
- interpreter = "python3";
413
- } catch {
414
- interpreter = "python";
415
- }
416
- } else if (lang === "node") {
417
- interpreter = "node";
418
- } else {
419
- interpreter = "bash";
420
- }
421
-
422
- const start = Date.now();
423
- const output = execSync(`${interpreter} "${tmpFile}"`, {
424
- cwd,
425
- encoding: "utf8",
426
- timeout: timeoutSec * 1000,
427
- maxBuffer: 5 * 1024 * 1024,
428
- });
429
- const duration = Date.now() - start;
430
-
431
- return {
432
- success: true,
433
- output: output.substring(0, 50000),
434
- language: lang,
435
- duration: `${duration}ms`,
436
- };
437
- } catch (err) {
438
- return {
439
- error: (err.stderr || err.message || "").substring(0, 5000),
440
- stderr: (err.stderr || "").substring(0, 5000),
441
- exitCode: err.status,
442
- language: lang,
443
- };
444
- } finally {
445
- try {
446
- fs.unlinkSync(tmpFile);
447
- } catch {
448
- // Cleanup best-effort
449
- }
450
- }
484
+ return _executeRunCode(args, cwd);
451
485
  }
452
486
 
453
487
  case "search_files": {
@@ -572,6 +606,224 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
572
606
  }
573
607
  }
574
608
 
609
+ // ─── run_code implementation ──────────────────────────────────────────────
610
+
611
+ /**
612
+ * Classify an error from code execution into a structured type with hints.
613
+ * @param {string} stderr - stderr output
614
+ * @param {string} message - error message
615
+ * @param {number|null} exitCode - process exit code
616
+ * @param {string} lang - language (python, node, bash)
617
+ * @returns {{ errorType: string, hint: string }}
618
+ */
619
+ export function classifyError(stderr, message, exitCode, lang) {
620
+ const text = stderr || message || "";
621
+
622
+ // Import / module errors
623
+ if (/ModuleNotFoundError|ImportError|No module named/i.test(text)) {
624
+ const modMatch = text.match(/No module named ['"]([^'"]+)['"]/);
625
+ return {
626
+ errorType: "import_error",
627
+ hint: modMatch
628
+ ? `Missing Python module "${modMatch[1]}". Will attempt auto-install.`
629
+ : "Missing module. Check your imports.",
630
+ };
631
+ }
632
+
633
+ // Syntax errors
634
+ if (/SyntaxError|IndentationError|TabError/i.test(text)) {
635
+ const lineMatch = text.match(/line (\d+)/i);
636
+ return {
637
+ errorType: "syntax_error",
638
+ hint: lineMatch
639
+ ? `Syntax error on line ${lineMatch[1]}. Check for typos, missing colons, or indentation.`
640
+ : "Syntax error in code. Check for typos or missing brackets.",
641
+ };
642
+ }
643
+
644
+ // Timeout
645
+ if (/ETIMEDOUT|timed?\s*out/i.test(text) || exitCode === null) {
646
+ return {
647
+ errorType: "timeout",
648
+ hint: "Script timed out. Consider increasing timeout or optimizing the code.",
649
+ };
650
+ }
651
+
652
+ // Permission errors
653
+ if (/EACCES|Permission denied|PermissionError/i.test(text)) {
654
+ return {
655
+ errorType: "permission_error",
656
+ hint: "Permission denied. Try a different directory or run with appropriate permissions.",
657
+ };
658
+ }
659
+
660
+ // Generic runtime error
661
+ const lineMatch = text.match(/(?:line |:)(\d+)/);
662
+ return {
663
+ errorType: "runtime_error",
664
+ hint: lineMatch
665
+ ? `Runtime error near line ${lineMatch[1]}. Check the traceback above.`
666
+ : "Runtime error. Check stderr for details.",
667
+ };
668
+ }
669
+
670
+ /**
671
+ * Validate a package name for pip install (reject shell metacharacters).
672
+ * @param {string} name
673
+ * @returns {boolean}
674
+ */
675
+ export function isValidPackageName(name) {
676
+ return /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/.test(name) && name.length <= 100;
677
+ }
678
+
679
+ /**
680
+ * Execute code with auto pip-install, script persistence, and error classification.
681
+ */
682
+ async function _executeRunCode(args, cwd) {
683
+ const lang = args.language;
684
+ const code = args.code;
685
+ const timeoutSec = Math.min(Math.max(args.timeout || 60, 1), 300);
686
+ const persist = args.persist !== false; // default true
687
+
688
+ const extMap = { python: ".py", node: ".js", bash: ".sh" };
689
+ const ext = extMap[lang];
690
+ if (!ext) {
691
+ return {
692
+ error: `Unsupported language: ${lang}. Use python, node, or bash.`,
693
+ };
694
+ }
695
+
696
+ // Determine script path
697
+ let scriptPath;
698
+ if (persist) {
699
+ const scriptsDir = path.join(cwd, ".chainlesschain", "agent-scripts");
700
+ if (!fs.existsSync(scriptsDir)) {
701
+ fs.mkdirSync(scriptsDir, { recursive: true });
702
+ }
703
+ const timestamp = new Date()
704
+ .toISOString()
705
+ .replace(/[T:]/g, "-")
706
+ .replace(/\.\d+Z$/, "");
707
+ scriptPath = path.join(scriptsDir, `${timestamp}-${lang}${ext}`);
708
+ } else {
709
+ scriptPath = path.join(os.tmpdir(), `cc-agent-${Date.now()}${ext}`);
710
+ }
711
+
712
+ try {
713
+ fs.writeFileSync(scriptPath, code, "utf8");
714
+
715
+ // Determine interpreter
716
+ let interpreter;
717
+ if (lang === "python") {
718
+ const py = getCachedPython();
719
+ interpreter = py.found ? py.command : "python";
720
+ } else if (lang === "node") {
721
+ interpreter = "node";
722
+ } else {
723
+ interpreter = "bash";
724
+ }
725
+
726
+ const start = Date.now();
727
+ let output;
728
+ try {
729
+ output = execSync(`${interpreter} "${scriptPath}"`, {
730
+ cwd,
731
+ encoding: "utf8",
732
+ timeout: timeoutSec * 1000,
733
+ maxBuffer: 5 * 1024 * 1024,
734
+ });
735
+ } catch (err) {
736
+ const stderr = (err.stderr || "").toString();
737
+ const message = err.message || "";
738
+ const classified = classifyError(stderr, message, err.status, lang);
739
+
740
+ // Auto-install missing Python packages
741
+ if (lang === "python" && classified.errorType === "import_error") {
742
+ const modMatch = stderr.match(/No module named ['"]([^'"]+)['"]/);
743
+ if (modMatch) {
744
+ // Use top-level package name (e.g. "foo.bar" → "foo")
745
+ const packageName = modMatch[1].split(".")[0];
746
+
747
+ if (!isValidPackageName(packageName)) {
748
+ return {
749
+ error: `Invalid package name: "${packageName}"`,
750
+ ...classified,
751
+ language: lang,
752
+ scriptPath: persist ? scriptPath : undefined,
753
+ };
754
+ }
755
+
756
+ // Attempt pip install
757
+ try {
758
+ execSync(`${interpreter} -m pip install ${packageName}`, {
759
+ encoding: "utf-8",
760
+ timeout: 120000,
761
+ maxBuffer: 2 * 1024 * 1024,
762
+ stdio: ["pipe", "pipe", "pipe"],
763
+ });
764
+
765
+ // Retry execution
766
+ const retryStart = Date.now();
767
+ const retryOutput = execSync(`${interpreter} "${scriptPath}"`, {
768
+ cwd,
769
+ encoding: "utf8",
770
+ timeout: timeoutSec * 1000,
771
+ maxBuffer: 5 * 1024 * 1024,
772
+ });
773
+ const retryDuration = Date.now() - retryStart;
774
+
775
+ return {
776
+ success: true,
777
+ output: retryOutput.substring(0, 50000),
778
+ language: lang,
779
+ duration: `${retryDuration}ms`,
780
+ autoInstalled: [packageName],
781
+ scriptPath: persist ? scriptPath : undefined,
782
+ };
783
+ } catch (pipErr) {
784
+ return {
785
+ error: (stderr || message).substring(0, 5000),
786
+ stderr: stderr.substring(0, 5000),
787
+ exitCode: err.status,
788
+ language: lang,
789
+ ...classified,
790
+ hint: `Failed to auto-install "${packageName}". ${(pipErr.stderr || pipErr.message || "").substring(0, 500)}`,
791
+ scriptPath: persist ? scriptPath : undefined,
792
+ };
793
+ }
794
+ }
795
+ }
796
+
797
+ return {
798
+ error: (stderr || message).substring(0, 5000),
799
+ stderr: stderr.substring(0, 5000),
800
+ exitCode: err.status,
801
+ language: lang,
802
+ ...classified,
803
+ scriptPath: persist ? scriptPath : undefined,
804
+ };
805
+ }
806
+
807
+ const duration = Date.now() - start;
808
+ return {
809
+ success: true,
810
+ output: output.substring(0, 50000),
811
+ language: lang,
812
+ duration: `${duration}ms`,
813
+ scriptPath: persist ? scriptPath : undefined,
814
+ };
815
+ } finally {
816
+ // Only clean up if not persisting
817
+ if (!persist) {
818
+ try {
819
+ fs.unlinkSync(scriptPath);
820
+ } catch {
821
+ // Cleanup best-effort
822
+ }
823
+ }
824
+ }
825
+ }
826
+
575
827
  // ─── LLM chat with tools ─────────────────────────────────────────────────
576
828
 
577
829
  /**
@@ -753,12 +1005,13 @@ function _normalizeAnthropicResponse(data) {
753
1005
  * Async generator that drives the agentic tool-use loop.
754
1006
  *
755
1007
  * Yields events:
1008
+ * { type: "slot-filling", slot, question } — when asking user for missing info
756
1009
  * { type: "tool-executing", tool, args }
757
1010
  * { type: "tool-result", tool, result, error }
758
1011
  * { type: "response-complete", content }
759
1012
  *
760
1013
  * @param {Array} messages - mutable messages array (will be appended to)
761
- * @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd
1014
+ * @param {object} options - provider, model, baseUrl, apiKey, contextEngine, hookDb, skillLoader, cwd, slotFiller, interaction
762
1015
  */
763
1016
  export async function* agentLoop(messages, options) {
764
1017
  const MAX_ITERATIONS = 15;
@@ -768,6 +1021,52 @@ export async function* agentLoop(messages, options) {
768
1021
  cwd: options.cwd || process.cwd(),
769
1022
  };
770
1023
 
1024
+ // ── Slot-filling phase ──────────────────────────────────────────────
1025
+ // Before calling the LLM, check if the user's message matches a known
1026
+ // intent with missing required parameters. If so, interactively fill them
1027
+ // and append the gathered context to the user message.
1028
+ if (options.slotFiller && options.interaction) {
1029
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
1030
+ if (lastUserMsg) {
1031
+ try {
1032
+ const { CLISlotFiller } = await import("./slot-filler.js");
1033
+ const intent = CLISlotFiller.detectIntent(lastUserMsg.content);
1034
+
1035
+ if (intent) {
1036
+ const requiredSlots = CLISlotFiller.getSlotDefinitions(
1037
+ intent.type,
1038
+ ).required;
1039
+ const missingSlots = requiredSlots.filter((s) => !intent.entities[s]);
1040
+
1041
+ if (missingSlots.length > 0) {
1042
+ const result = await options.slotFiller.fillSlots(intent, {
1043
+ cwd: options.cwd || process.cwd(),
1044
+ });
1045
+
1046
+ // Yield slot-filling events for each filled slot
1047
+ for (const slot of result.filledSlots) {
1048
+ yield {
1049
+ type: "slot-filling",
1050
+ slot,
1051
+ question: `Filled "${slot}" = "${result.entities[slot]}"`,
1052
+ };
1053
+ }
1054
+
1055
+ // Append gathered context to the user message so the LLM has full info
1056
+ if (result.filledSlots.length > 0) {
1057
+ const contextParts = Object.entries(result.entities)
1058
+ .filter(([, v]) => v)
1059
+ .map(([k, v]) => `${k}: ${v}`);
1060
+ lastUserMsg.content += `\n\n[Context — user provided: ${contextParts.join(", ")}]`;
1061
+ }
1062
+ }
1063
+ }
1064
+ } catch (_err) {
1065
+ // Slot-filling failure is non-critical — proceed to LLM
1066
+ }
1067
+ }
1068
+ }
1069
+
771
1070
  for (let i = 0; i < MAX_ITERATIONS; i++) {
772
1071
  const result = await chatWithTools(messages, options);
773
1072
  const msg = result?.message;