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 +3 -3
- package/package.json +1 -1
- package/src/commands/serve.js +34 -1
- package/src/lib/agent-core.js +361 -62
- package/src/lib/slot-filler.js +133 -0
- package/src/lib/ws-agent-handler.js +17 -0
- package/src/lib/ws-server.js +30 -2
- package/src/repl/agent-repl.js +57 -808
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 (
|
|
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** | **
|
|
929
|
+
| **CLI Total** | **113** | **2503** | **All passing** |
|
|
930
930
|
|
|
931
931
|
## License
|
|
932
932
|
|
package/package.json
CHANGED
package/src/commands/serve.js
CHANGED
|
@@ -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("");
|
package/src/lib/agent-core.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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;
|