chainlesschain 0.40.2 → 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 +23 -16
- package/package.json +1 -1
- package/src/commands/serve.js +34 -1
- package/src/lib/agent-core.js +1160 -0
- package/src/lib/chat-core.js +177 -0
- package/src/lib/interaction-adapter.js +177 -0
- package/src/lib/interactive-planner.js +524 -0
- package/src/lib/llm-providers.js +9 -1
- package/src/lib/slot-filler.js +598 -0
- package/src/lib/task-model-selector.js +5 -5
- package/src/lib/ws-agent-handler.js +420 -0
- package/src/lib/ws-chat-handler.js +145 -0
- package/src/lib/ws-server.js +308 -1
- package/src/lib/ws-session-manager.js +363 -0
- package/src/repl/agent-repl.js +112 -715
package/src/repl/agent-repl.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* - run_shell: Execute a shell command
|
|
11
11
|
* - search_files: Search for files by name/content
|
|
12
12
|
* - list_dir: List directory contents
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
13
|
+
* - run_skill: Run a built-in skill
|
|
14
|
+
* - list_skills: List available skills
|
|
15
|
+
* - run_code: Write and execute code (Python/Node.js/Bash)
|
|
16
16
|
*
|
|
17
17
|
* The AI decides which tools to call based on user intent.
|
|
18
18
|
*/
|
|
@@ -21,11 +21,8 @@ import readline from "readline";
|
|
|
21
21
|
import chalk from "chalk";
|
|
22
22
|
import fs from "fs";
|
|
23
23
|
import path from "path";
|
|
24
|
-
import { execSync } from "child_process";
|
|
25
|
-
import { fileURLToPath } from "url";
|
|
26
24
|
import { logger } from "../lib/logger.js";
|
|
27
25
|
import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
|
|
28
|
-
import { CLISkillLoader } from "../lib/skill-loader.js";
|
|
29
26
|
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
30
27
|
import {
|
|
31
28
|
createSession,
|
|
@@ -39,176 +36,15 @@ import {
|
|
|
39
36
|
detectTaskType,
|
|
40
37
|
selectModelForTask,
|
|
41
38
|
} from "../lib/task-model-selector.js";
|
|
42
|
-
import { executeHooks, HookEvents } from "../lib/hook-manager.js";
|
|
43
39
|
import { CLIPermanentMemory } from "../lib/permanent-memory.js";
|
|
44
40
|
import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function: {
|
|
53
|
-
name: "read_file",
|
|
54
|
-
description: "Read a file's content",
|
|
55
|
-
parameters: {
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: {
|
|
58
|
-
path: { type: "string", description: "File path to read" },
|
|
59
|
-
},
|
|
60
|
-
required: ["path"],
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: "function",
|
|
66
|
-
function: {
|
|
67
|
-
name: "write_file",
|
|
68
|
-
description: "Write content to a file (create or overwrite)",
|
|
69
|
-
parameters: {
|
|
70
|
-
type: "object",
|
|
71
|
-
properties: {
|
|
72
|
-
path: { type: "string", description: "File path" },
|
|
73
|
-
content: { type: "string", description: "File content" },
|
|
74
|
-
},
|
|
75
|
-
required: ["path", "content"],
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
type: "function",
|
|
81
|
-
function: {
|
|
82
|
-
name: "edit_file",
|
|
83
|
-
description: "Replace a specific string in a file with new content",
|
|
84
|
-
parameters: {
|
|
85
|
-
type: "object",
|
|
86
|
-
properties: {
|
|
87
|
-
path: { type: "string", description: "File path" },
|
|
88
|
-
old_string: {
|
|
89
|
-
type: "string",
|
|
90
|
-
description: "Exact string to find and replace",
|
|
91
|
-
},
|
|
92
|
-
new_string: {
|
|
93
|
-
type: "string",
|
|
94
|
-
description: "Replacement string",
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
required: ["path", "old_string", "new_string"],
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
type: "function",
|
|
103
|
-
function: {
|
|
104
|
-
name: "run_shell",
|
|
105
|
-
description:
|
|
106
|
-
"Execute a shell command and return the output. Use for running tests, installing packages, git operations, etc.",
|
|
107
|
-
parameters: {
|
|
108
|
-
type: "object",
|
|
109
|
-
properties: {
|
|
110
|
-
command: { type: "string", description: "Shell command to execute" },
|
|
111
|
-
cwd: {
|
|
112
|
-
type: "string",
|
|
113
|
-
description: "Working directory (optional)",
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
required: ["command"],
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
type: "function",
|
|
122
|
-
function: {
|
|
123
|
-
name: "search_files",
|
|
124
|
-
description: "Search for files by name pattern or content",
|
|
125
|
-
parameters: {
|
|
126
|
-
type: "object",
|
|
127
|
-
properties: {
|
|
128
|
-
pattern: {
|
|
129
|
-
type: "string",
|
|
130
|
-
description: "Glob pattern or search string",
|
|
131
|
-
},
|
|
132
|
-
directory: {
|
|
133
|
-
type: "string",
|
|
134
|
-
description: "Directory to search in (default: cwd)",
|
|
135
|
-
},
|
|
136
|
-
content_search: {
|
|
137
|
-
type: "boolean",
|
|
138
|
-
description: "If true, search file contents instead of names",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
required: ["pattern"],
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
type: "function",
|
|
147
|
-
function: {
|
|
148
|
-
name: "list_dir",
|
|
149
|
-
description: "List contents of a directory",
|
|
150
|
-
parameters: {
|
|
151
|
-
type: "object",
|
|
152
|
-
properties: {
|
|
153
|
-
path: {
|
|
154
|
-
type: "string",
|
|
155
|
-
description: "Directory path (default: cwd)",
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
type: "function",
|
|
163
|
-
function: {
|
|
164
|
-
name: "run_skill",
|
|
165
|
-
description:
|
|
166
|
-
"Run a built-in ChainlessChain skill. Available skills include: code-review, summarize, translate, refactor, unit-test, debug, explain-code, browser-automation, data-analysis, git-history-analyzer, and 130+ more. Use list_skills first to discover available skills.",
|
|
167
|
-
parameters: {
|
|
168
|
-
type: "object",
|
|
169
|
-
properties: {
|
|
170
|
-
skill_name: {
|
|
171
|
-
type: "string",
|
|
172
|
-
description:
|
|
173
|
-
"Name of the skill to run (e.g. code-review, summarize, translate)",
|
|
174
|
-
},
|
|
175
|
-
input: {
|
|
176
|
-
type: "string",
|
|
177
|
-
description: "Input text or parameters for the skill",
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
required: ["skill_name", "input"],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
type: "function",
|
|
186
|
-
function: {
|
|
187
|
-
name: "list_skills",
|
|
188
|
-
description:
|
|
189
|
-
"List available built-in skills, optionally filtered by category or keyword",
|
|
190
|
-
parameters: {
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: {
|
|
193
|
-
category: {
|
|
194
|
-
type: "string",
|
|
195
|
-
description:
|
|
196
|
-
"Filter by category (e.g. development, automation, data)",
|
|
197
|
-
},
|
|
198
|
-
query: {
|
|
199
|
-
type: "string",
|
|
200
|
-
description: "Search keyword to filter skills",
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Shared multi-layer skill loader
|
|
210
|
-
*/
|
|
211
|
-
const skillLoader = new CLISkillLoader();
|
|
41
|
+
import {
|
|
42
|
+
AGENT_TOOLS,
|
|
43
|
+
getBaseSystemPrompt,
|
|
44
|
+
executeTool as coreExecuteTool,
|
|
45
|
+
agentLoop as coreAgentLoop,
|
|
46
|
+
formatToolArgs,
|
|
47
|
+
} from "../lib/agent-core.js";
|
|
212
48
|
|
|
213
49
|
/**
|
|
214
50
|
* Reference to the runtime DB for hook execution (set during startAgentRepl)
|
|
@@ -216,564 +52,43 @@ const skillLoader = new CLISkillLoader();
|
|
|
216
52
|
let _hookDb = null;
|
|
217
53
|
|
|
218
54
|
/**
|
|
219
|
-
* Execute a tool call
|
|
55
|
+
* Execute a tool call — delegates to agent-core with REPL's hookDb and cwd.
|
|
220
56
|
*/
|
|
221
57
|
async function executeTool(name, args) {
|
|
222
|
-
|
|
223
|
-
const planManager = getPlanModeManager();
|
|
224
|
-
if (planManager.isActive() && !planManager.isToolAllowed(name)) {
|
|
225
|
-
// In plan mode, log the blocked tool as a plan item
|
|
226
|
-
planManager.addPlanItem({
|
|
227
|
-
title: `${name}: ${formatToolArgs(name, args)}`,
|
|
228
|
-
tool: name,
|
|
229
|
-
params: args,
|
|
230
|
-
estimatedImpact:
|
|
231
|
-
name === "run_shell"
|
|
232
|
-
? "high"
|
|
233
|
-
: name === "write_file"
|
|
234
|
-
? "medium"
|
|
235
|
-
: "low",
|
|
236
|
-
});
|
|
237
|
-
return {
|
|
238
|
-
error: `[Plan Mode] Tool "${name}" is blocked during planning. It has been added to the plan. Use /plan approve to execute.`,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// PreToolUse hook
|
|
243
|
-
if (_hookDb) {
|
|
244
|
-
try {
|
|
245
|
-
await executeHooks(_hookDb, HookEvents.PreToolUse, {
|
|
246
|
-
tool: name,
|
|
247
|
-
args,
|
|
248
|
-
timestamp: new Date().toISOString(),
|
|
249
|
-
});
|
|
250
|
-
} catch (_err) {
|
|
251
|
-
// Hook failure should not block tool execution
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
let toolResult;
|
|
256
|
-
try {
|
|
257
|
-
toolResult = await _executeToolInner(name, args);
|
|
258
|
-
} catch (err) {
|
|
259
|
-
// ToolError hook
|
|
260
|
-
if (_hookDb) {
|
|
261
|
-
try {
|
|
262
|
-
await executeHooks(_hookDb, HookEvents.ToolError, {
|
|
263
|
-
tool: name,
|
|
264
|
-
args,
|
|
265
|
-
error: err.message,
|
|
266
|
-
});
|
|
267
|
-
} catch (_err) {
|
|
268
|
-
// Non-critical
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
throw err;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// PostToolUse hook
|
|
275
|
-
if (_hookDb) {
|
|
276
|
-
try {
|
|
277
|
-
await executeHooks(_hookDb, HookEvents.PostToolUse, {
|
|
278
|
-
tool: name,
|
|
279
|
-
args,
|
|
280
|
-
result:
|
|
281
|
-
typeof toolResult === "object"
|
|
282
|
-
? JSON.stringify(toolResult).substring(0, 500)
|
|
283
|
-
: String(toolResult).substring(0, 500),
|
|
284
|
-
});
|
|
285
|
-
} catch (_err) {
|
|
286
|
-
// Non-critical
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return toolResult;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Inner tool execution logic (separated for hook wrapping)
|
|
295
|
-
*/
|
|
296
|
-
async function _executeToolInner(name, args) {
|
|
297
|
-
switch (name) {
|
|
298
|
-
case "read_file": {
|
|
299
|
-
const filePath = path.resolve(args.path);
|
|
300
|
-
if (!fs.existsSync(filePath)) {
|
|
301
|
-
return { error: `File not found: ${filePath}` };
|
|
302
|
-
}
|
|
303
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
304
|
-
// Truncate very long files
|
|
305
|
-
if (content.length > 50000) {
|
|
306
|
-
return {
|
|
307
|
-
content: content.substring(0, 50000) + "\n...(truncated)",
|
|
308
|
-
size: content.length,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
return { content };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
case "write_file": {
|
|
315
|
-
const filePath = path.resolve(args.path);
|
|
316
|
-
const dir = path.dirname(filePath);
|
|
317
|
-
if (!fs.existsSync(dir)) {
|
|
318
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
319
|
-
}
|
|
320
|
-
fs.writeFileSync(filePath, args.content, "utf8");
|
|
321
|
-
return { success: true, path: filePath, size: args.content.length };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
case "edit_file": {
|
|
325
|
-
const filePath = path.resolve(args.path);
|
|
326
|
-
if (!fs.existsSync(filePath)) {
|
|
327
|
-
return { error: `File not found: ${filePath}` };
|
|
328
|
-
}
|
|
329
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
330
|
-
if (!content.includes(args.old_string)) {
|
|
331
|
-
return { error: "old_string not found in file" };
|
|
332
|
-
}
|
|
333
|
-
const newContent = content.replace(args.old_string, args.new_string);
|
|
334
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
335
|
-
return { success: true, path: filePath };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
case "run_shell": {
|
|
339
|
-
try {
|
|
340
|
-
const output = execSync(args.command, {
|
|
341
|
-
cwd: args.cwd || process.cwd(),
|
|
342
|
-
encoding: "utf8",
|
|
343
|
-
timeout: 30000,
|
|
344
|
-
maxBuffer: 1024 * 1024,
|
|
345
|
-
});
|
|
346
|
-
return { stdout: output.substring(0, 10000) };
|
|
347
|
-
} catch (err) {
|
|
348
|
-
return {
|
|
349
|
-
error: err.message.substring(0, 2000),
|
|
350
|
-
stderr: (err.stderr || "").substring(0, 2000),
|
|
351
|
-
exitCode: err.status,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
case "search_files": {
|
|
357
|
-
const dir = args.directory ? path.resolve(args.directory) : process.cwd();
|
|
358
|
-
try {
|
|
359
|
-
if (args.content_search) {
|
|
360
|
-
// Use grep/findstr for content search
|
|
361
|
-
const cmd =
|
|
362
|
-
process.platform === "win32"
|
|
363
|
-
? `findstr /s /i /n "${args.pattern}" *`
|
|
364
|
-
: `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`;
|
|
365
|
-
const output = execSync(cmd, {
|
|
366
|
-
cwd: dir,
|
|
367
|
-
encoding: "utf8",
|
|
368
|
-
timeout: 10000,
|
|
369
|
-
});
|
|
370
|
-
return { matches: output.trim().split("\n").slice(0, 20) };
|
|
371
|
-
} else {
|
|
372
|
-
// File name search
|
|
373
|
-
const cmd =
|
|
374
|
-
process.platform === "win32"
|
|
375
|
-
? `dir /s /b *${args.pattern}* 2>NUL`
|
|
376
|
-
: `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
|
|
377
|
-
const output = execSync(cmd, {
|
|
378
|
-
cwd: dir,
|
|
379
|
-
encoding: "utf8",
|
|
380
|
-
timeout: 10000,
|
|
381
|
-
});
|
|
382
|
-
return {
|
|
383
|
-
files: output.trim().split("\n").filter(Boolean).slice(0, 20),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
return { files: [], message: "No matches found" };
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
case "list_dir": {
|
|
392
|
-
const dirPath = args.path ? path.resolve(args.path) : process.cwd();
|
|
393
|
-
if (!fs.existsSync(dirPath)) {
|
|
394
|
-
return { error: `Directory not found: ${dirPath}` };
|
|
395
|
-
}
|
|
396
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
397
|
-
return {
|
|
398
|
-
entries: entries.map((e) => ({
|
|
399
|
-
name: e.name,
|
|
400
|
-
type: e.isDirectory() ? "dir" : "file",
|
|
401
|
-
})),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
case "run_skill": {
|
|
406
|
-
const allSkills = skillLoader.getResolvedSkills();
|
|
407
|
-
if (allSkills.length === 0) {
|
|
408
|
-
return {
|
|
409
|
-
error:
|
|
410
|
-
"No skills found. Make sure you're in the ChainlessChain project root or have skills installed.",
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const match = allSkills.find(
|
|
414
|
-
(s) => s.id === args.skill_name || s.dirName === args.skill_name,
|
|
415
|
-
);
|
|
416
|
-
if (!match || !match.hasHandler) {
|
|
417
|
-
return {
|
|
418
|
-
error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
const handlerPath = path.join(match.skillDir, "handler.js");
|
|
423
|
-
const imported = await import(
|
|
424
|
-
`file://${handlerPath.replace(/\\/g, "/")}`
|
|
425
|
-
);
|
|
426
|
-
const handler = imported.default || imported;
|
|
427
|
-
if (handler.init) await handler.init(match);
|
|
428
|
-
const task = {
|
|
429
|
-
params: { input: args.input },
|
|
430
|
-
input: args.input,
|
|
431
|
-
action: args.input,
|
|
432
|
-
};
|
|
433
|
-
const context = {
|
|
434
|
-
projectRoot: process.cwd(),
|
|
435
|
-
workspacePath: process.cwd(),
|
|
436
|
-
};
|
|
437
|
-
const result = await handler.execute(task, context, match);
|
|
438
|
-
return result;
|
|
439
|
-
} catch (err) {
|
|
440
|
-
return { error: `Skill execution failed: ${err.message}` };
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
case "list_skills": {
|
|
445
|
-
let skills = skillLoader.getResolvedSkills();
|
|
446
|
-
if (skills.length === 0) {
|
|
447
|
-
return { error: "No skills found." };
|
|
448
|
-
}
|
|
449
|
-
if (args.category) {
|
|
450
|
-
skills = skills.filter(
|
|
451
|
-
(s) => s.category.toLowerCase() === args.category.toLowerCase(),
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
if (args.query) {
|
|
455
|
-
const q = args.query.toLowerCase();
|
|
456
|
-
skills = skills.filter(
|
|
457
|
-
(s) =>
|
|
458
|
-
s.id.includes(q) ||
|
|
459
|
-
s.description.toLowerCase().includes(q) ||
|
|
460
|
-
s.category.toLowerCase().includes(q),
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
return {
|
|
464
|
-
count: skills.length,
|
|
465
|
-
skills: skills.map((s) => ({
|
|
466
|
-
id: s.id,
|
|
467
|
-
category: s.category,
|
|
468
|
-
source: s.source,
|
|
469
|
-
hasHandler: s.hasHandler,
|
|
470
|
-
description: (s.description || "").substring(0, 80),
|
|
471
|
-
})),
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
default:
|
|
476
|
-
return { error: `Unknown tool: ${name}` };
|
|
477
|
-
}
|
|
58
|
+
return coreExecuteTool(name, args, { hookDb: _hookDb, cwd: process.cwd() });
|
|
478
59
|
}
|
|
479
60
|
|
|
480
61
|
/**
|
|
481
|
-
*
|
|
482
|
-
* Supports all 7 providers via cowork-adapter: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral
|
|
483
|
-
*/
|
|
484
|
-
async function chatWithTools(rawMessages, options) {
|
|
485
|
-
const { provider, model, baseUrl, apiKey, contextEngine: ce } = options;
|
|
486
|
-
|
|
487
|
-
// Build optimized messages via context engine (or use raw)
|
|
488
|
-
// Find last user message for relevance matching (not tool/assistant)
|
|
489
|
-
const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
|
|
490
|
-
const messages = ce
|
|
491
|
-
? ce.buildOptimizedMessages(rawMessages, {
|
|
492
|
-
userQuery: lastUserMsg?.content,
|
|
493
|
-
})
|
|
494
|
-
: rawMessages;
|
|
495
|
-
|
|
496
|
-
if (provider === "ollama") {
|
|
497
|
-
// Ollama supports tool calling natively
|
|
498
|
-
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
499
|
-
method: "POST",
|
|
500
|
-
headers: { "Content-Type": "application/json" },
|
|
501
|
-
body: JSON.stringify({
|
|
502
|
-
model,
|
|
503
|
-
messages,
|
|
504
|
-
tools: TOOLS,
|
|
505
|
-
stream: false,
|
|
506
|
-
}),
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
if (!response.ok) {
|
|
510
|
-
throw new Error(`Ollama error: ${response.status}`);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return await response.json();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (provider === "anthropic") {
|
|
517
|
-
// Anthropic: extract system messages, use tools format
|
|
518
|
-
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
519
|
-
if (!key) throw new Error("ANTHROPIC_API_KEY required");
|
|
520
|
-
|
|
521
|
-
const systemMsgs = messages.filter((m) => m.role === "system");
|
|
522
|
-
const otherMsgs = messages.filter((m) => m.role !== "system");
|
|
523
|
-
|
|
524
|
-
// Convert TOOLS to Anthropic format
|
|
525
|
-
const anthropicTools = TOOLS.map((t) => ({
|
|
526
|
-
name: t.function.name,
|
|
527
|
-
description: t.function.description,
|
|
528
|
-
input_schema: t.function.parameters,
|
|
529
|
-
}));
|
|
530
|
-
|
|
531
|
-
const body = {
|
|
532
|
-
model: model || "claude-sonnet-4-20250514",
|
|
533
|
-
max_tokens: 4096,
|
|
534
|
-
messages: otherMsgs,
|
|
535
|
-
tools: anthropicTools,
|
|
536
|
-
};
|
|
537
|
-
if (systemMsgs.length > 0) {
|
|
538
|
-
body.system = systemMsgs.map((m) => m.content).join("\n");
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const url =
|
|
542
|
-
baseUrl && baseUrl !== "http://localhost:11434"
|
|
543
|
-
? baseUrl
|
|
544
|
-
: "https://api.anthropic.com/v1";
|
|
545
|
-
|
|
546
|
-
const response = await fetch(`${url}/messages`, {
|
|
547
|
-
method: "POST",
|
|
548
|
-
headers: {
|
|
549
|
-
"Content-Type": "application/json",
|
|
550
|
-
"x-api-key": key,
|
|
551
|
-
"anthropic-version": "2023-06-01",
|
|
552
|
-
},
|
|
553
|
-
body: JSON.stringify(body),
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
if (!response.ok) {
|
|
557
|
-
throw new Error(`Anthropic error: ${response.status}`);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const data = await response.json();
|
|
561
|
-
// Normalize Anthropic response to Ollama-like format
|
|
562
|
-
return _normalizeAnthropicResponse(data);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// OpenAI-compatible providers (openai, deepseek, dashscope, mistral, gemini)
|
|
566
|
-
const providerUrls = {
|
|
567
|
-
openai: "https://api.openai.com/v1",
|
|
568
|
-
deepseek: "https://api.deepseek.com/v1",
|
|
569
|
-
dashscope: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
570
|
-
mistral: "https://api.mistral.ai/v1",
|
|
571
|
-
gemini: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
572
|
-
volcengine: "https://ark.cn-beijing.volces.com/api/v3",
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
const providerApiKeyEnvs = {
|
|
576
|
-
openai: "OPENAI_API_KEY",
|
|
577
|
-
deepseek: "DEEPSEEK_API_KEY",
|
|
578
|
-
dashscope: "DASHSCOPE_API_KEY",
|
|
579
|
-
mistral: "MISTRAL_API_KEY",
|
|
580
|
-
gemini: "GEMINI_API_KEY",
|
|
581
|
-
volcengine: "VOLCENGINE_API_KEY",
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
const url =
|
|
585
|
-
baseUrl && baseUrl !== "http://localhost:11434"
|
|
586
|
-
? baseUrl
|
|
587
|
-
: providerUrls[provider];
|
|
588
|
-
|
|
589
|
-
if (!url) {
|
|
590
|
-
throw new Error(
|
|
591
|
-
`Unsupported provider: ${provider}. Supported: ollama, anthropic, openai, deepseek, dashscope, mistral, gemini, volcengine`,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const envKey = providerApiKeyEnvs[provider] || "OPENAI_API_KEY";
|
|
596
|
-
const key = apiKey || process.env[envKey];
|
|
597
|
-
if (!key) throw new Error(`${envKey} required for provider ${provider}`);
|
|
598
|
-
|
|
599
|
-
const defaultModels = {
|
|
600
|
-
openai: "gpt-4o-mini",
|
|
601
|
-
deepseek: "deepseek-chat",
|
|
602
|
-
dashscope: "qwen-turbo",
|
|
603
|
-
mistral: "mistral-large-latest",
|
|
604
|
-
gemini: "gemini-2.0-flash",
|
|
605
|
-
volcengine: "doubao-seed-1-6-251015",
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
const response = await fetch(`${url}/chat/completions`, {
|
|
609
|
-
method: "POST",
|
|
610
|
-
headers: {
|
|
611
|
-
"Content-Type": "application/json",
|
|
612
|
-
Authorization: `Bearer ${key}`,
|
|
613
|
-
},
|
|
614
|
-
body: JSON.stringify({
|
|
615
|
-
model: model || defaultModels[provider] || "gpt-4o-mini",
|
|
616
|
-
messages,
|
|
617
|
-
tools: TOOLS,
|
|
618
|
-
}),
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
if (!response.ok) {
|
|
622
|
-
throw new Error(`${provider} API error: ${response.status}`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const data = await response.json();
|
|
626
|
-
// Normalize to Ollama-like format
|
|
627
|
-
if (!data.choices || !data.choices[0]) {
|
|
628
|
-
throw new Error("Invalid API response: no choices returned");
|
|
629
|
-
}
|
|
630
|
-
const choice = data.choices[0];
|
|
631
|
-
return {
|
|
632
|
-
message: choice.message,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Normalize Anthropic API response to Ollama-like format for uniform handling
|
|
638
|
-
*/
|
|
639
|
-
function _normalizeAnthropicResponse(data) {
|
|
640
|
-
const content = data.content || [];
|
|
641
|
-
const textBlocks = content.filter((b) => b.type === "text");
|
|
642
|
-
const toolBlocks = content.filter((b) => b.type === "tool_use");
|
|
643
|
-
|
|
644
|
-
const message = {
|
|
645
|
-
role: "assistant",
|
|
646
|
-
content: textBlocks.map((b) => b.text).join("\n") || "",
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
if (toolBlocks.length > 0) {
|
|
650
|
-
message.tool_calls = toolBlocks.map((b) => ({
|
|
651
|
-
id: b.id,
|
|
652
|
-
type: "function",
|
|
653
|
-
function: {
|
|
654
|
-
name: b.name,
|
|
655
|
-
arguments: JSON.stringify(b.input),
|
|
656
|
-
},
|
|
657
|
-
}));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return { message };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Agentic loop - keeps calling tools until the AI gives a final text response
|
|
62
|
+
* Agentic loop — wraps agent-core's async generator with REPL display output.
|
|
665
63
|
*/
|
|
666
64
|
async function agentLoop(messages, options) {
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
670
|
-
const result = await chatWithTools(messages, options);
|
|
671
|
-
const msg = result?.message;
|
|
672
|
-
|
|
673
|
-
if (!msg) {
|
|
674
|
-
return "(No response from LLM)";
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Check for tool calls
|
|
678
|
-
const toolCalls = msg.tool_calls;
|
|
679
|
-
|
|
680
|
-
if (!toolCalls || toolCalls.length === 0) {
|
|
681
|
-
// No tool calls — final text response
|
|
682
|
-
return msg.content || "";
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// Add assistant message with tool calls
|
|
686
|
-
messages.push(msg);
|
|
687
|
-
|
|
688
|
-
// Execute each tool call
|
|
689
|
-
for (const call of toolCalls) {
|
|
690
|
-
const fn = call.function;
|
|
691
|
-
const toolName = fn.name;
|
|
692
|
-
let toolArgs;
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
toolArgs =
|
|
696
|
-
typeof fn.arguments === "string"
|
|
697
|
-
? JSON.parse(fn.arguments)
|
|
698
|
-
: fn.arguments;
|
|
699
|
-
} catch {
|
|
700
|
-
toolArgs = {};
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Show what the AI is doing
|
|
65
|
+
for await (const event of coreAgentLoop(messages, options)) {
|
|
66
|
+
if (event.type === "tool-executing") {
|
|
704
67
|
process.stdout.write(
|
|
705
|
-
chalk.gray(
|
|
68
|
+
chalk.gray(
|
|
69
|
+
` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
|
|
70
|
+
),
|
|
706
71
|
);
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
} else if (toolResult.success) {
|
|
72
|
+
} else if (event.type === "tool-result") {
|
|
73
|
+
if (event.error || event.result?.error) {
|
|
74
|
+
process.stdout.write(
|
|
75
|
+
chalk.red(` Error: ${event.error || event.result?.error}\n`),
|
|
76
|
+
);
|
|
77
|
+
} else if (event.result?.success) {
|
|
714
78
|
process.stdout.write(chalk.green(` Done\n`));
|
|
715
79
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
messages.push({
|
|
719
|
-
role: "tool",
|
|
720
|
-
content: JSON.stringify(toolResult).substring(0, 5000),
|
|
721
|
-
tool_call_id: call.id,
|
|
722
|
-
});
|
|
80
|
+
} else if (event.type === "response-complete") {
|
|
81
|
+
return event.content;
|
|
723
82
|
}
|
|
724
83
|
}
|
|
725
|
-
|
|
726
|
-
return "(Reached max tool call iterations)";
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Format tool args for display
|
|
731
|
-
*/
|
|
732
|
-
function formatToolArgs(name, args) {
|
|
733
|
-
switch (name) {
|
|
734
|
-
case "read_file":
|
|
735
|
-
return args.path;
|
|
736
|
-
case "write_file":
|
|
737
|
-
return `${args.path} (${args.content?.length || 0} chars)`;
|
|
738
|
-
case "edit_file":
|
|
739
|
-
return args.path;
|
|
740
|
-
case "run_shell":
|
|
741
|
-
return args.command;
|
|
742
|
-
case "search_files":
|
|
743
|
-
return args.pattern;
|
|
744
|
-
case "list_dir":
|
|
745
|
-
return args.path || ".";
|
|
746
|
-
case "run_skill":
|
|
747
|
-
return `${args.skill_name}: ${(args.input || "").substring(0, 50)}`;
|
|
748
|
-
case "list_skills":
|
|
749
|
-
return args.category || args.query || "all";
|
|
750
|
-
default:
|
|
751
|
-
return JSON.stringify(args).substring(0, 60);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
function getBaseSystemPrompt() {
|
|
756
|
-
return `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
|
|
757
|
-
|
|
758
|
-
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.
|
|
759
|
-
|
|
760
|
-
Key behaviors:
|
|
761
|
-
- When asked to modify code, read the file first, then edit it
|
|
762
|
-
- When asked to create something, use write_file to create it
|
|
763
|
-
- When asked to run/test something, use run_shell to execute it
|
|
764
|
-
- When asked about files or code, use read_file and search_files to find information
|
|
765
|
-
- You have multi-layer skills (built-in, marketplace, global, project-level) — use list_skills to discover them and run_skill to execute them
|
|
766
|
-
- Always explain what you're doing and show results
|
|
767
|
-
- Be concise but thorough
|
|
768
|
-
|
|
769
|
-
Current working directory: ${process.cwd()}`;
|
|
84
|
+
return "";
|
|
770
85
|
}
|
|
771
86
|
|
|
772
87
|
/**
|
|
773
88
|
* Start the agentic REPL
|
|
774
89
|
*/
|
|
775
90
|
export async function startAgentRepl(options = {}) {
|
|
776
|
-
let model = options.model || "qwen2:7b";
|
|
91
|
+
let model = options.model || "qwen2.5:7b";
|
|
777
92
|
let provider = options.provider || "ollama";
|
|
778
93
|
const baseUrl = options.baseUrl || "http://localhost:11434";
|
|
779
94
|
const apiKey = options.apiKey || null;
|
|
@@ -834,7 +149,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
834
149
|
}
|
|
835
150
|
}
|
|
836
151
|
|
|
837
|
-
const messages = [
|
|
152
|
+
const messages = [
|
|
153
|
+
{ role: "system", content: getBaseSystemPrompt(process.cwd()) },
|
|
154
|
+
];
|
|
838
155
|
|
|
839
156
|
// Load resumed session messages
|
|
840
157
|
if (db && options.sessionId && sessionId) {
|
|
@@ -1551,9 +868,59 @@ export async function startAgentRepl(options = {}) {
|
|
|
1551
868
|
} else {
|
|
1552
869
|
logger.info("Not in plan mode.");
|
|
1553
870
|
}
|
|
871
|
+
} else if (subCmd.startsWith("interactive")) {
|
|
872
|
+
// Interactive planning with LLM-generated plan + skill recommendations
|
|
873
|
+
const planRequest =
|
|
874
|
+
subCmd.slice(11).trim() || "Help me with the current task";
|
|
875
|
+
try {
|
|
876
|
+
const { CLIInteractivePlanner } =
|
|
877
|
+
await import("../lib/interactive-planner.js");
|
|
878
|
+
const { TerminalInteractionAdapter } =
|
|
879
|
+
await import("../lib/interaction-adapter.js");
|
|
880
|
+
const chatFn = createChatFn({ provider, model, baseUrl, apiKey });
|
|
881
|
+
const planner = new CLIInteractivePlanner({
|
|
882
|
+
llmChat: chatFn,
|
|
883
|
+
db,
|
|
884
|
+
interaction: new TerminalInteractionAdapter(),
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
logger.info("Generating interactive plan...");
|
|
888
|
+
const result = await planner.startPlanSession(planRequest, {
|
|
889
|
+
cwd: process.cwd(),
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
if (result.plan) {
|
|
893
|
+
logger.log(
|
|
894
|
+
chalk.bold(
|
|
895
|
+
`\n Plan: ${result.plan.overview?.title || "Untitled"}`,
|
|
896
|
+
),
|
|
897
|
+
);
|
|
898
|
+
logger.log(
|
|
899
|
+
chalk.gray(` ${result.plan.overview?.description || ""}\n`),
|
|
900
|
+
);
|
|
901
|
+
for (const step of result.plan.steps || []) {
|
|
902
|
+
const toolStr = step.tool ? chalk.cyan(` [${step.tool}]`) : "";
|
|
903
|
+
logger.log(` ${step.step}. ${step.title}${toolStr}`);
|
|
904
|
+
}
|
|
905
|
+
if (result.plan.recommendations?.skills?.length > 0) {
|
|
906
|
+
logger.log(chalk.bold("\n Recommended skills:"));
|
|
907
|
+
for (const s of result.plan.recommendations.skills) {
|
|
908
|
+
logger.log(` - ${chalk.cyan(s.id)}: ${s.description}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
logger.log("");
|
|
912
|
+
logger.info(
|
|
913
|
+
"Use /plan interactive:confirm, /plan interactive:cancel, or /plan interactive:regenerate",
|
|
914
|
+
);
|
|
915
|
+
} else {
|
|
916
|
+
logger.info(result.message || "Failed to generate plan");
|
|
917
|
+
}
|
|
918
|
+
} catch (err) {
|
|
919
|
+
logger.error(`Interactive plan failed: ${err.message}`);
|
|
920
|
+
}
|
|
1554
921
|
} else {
|
|
1555
922
|
logger.info(
|
|
1556
|
-
"Unknown /plan subcommand. Try: /plan, /plan show, /plan approve, /plan reject, /plan exit",
|
|
923
|
+
"Unknown /plan subcommand. Try: /plan, /plan show, /plan approve, /plan reject, /plan exit, /plan interactive <request>",
|
|
1557
924
|
);
|
|
1558
925
|
}
|
|
1559
926
|
|
|
@@ -1564,6 +931,36 @@ export async function startAgentRepl(options = {}) {
|
|
|
1564
931
|
// Add user message
|
|
1565
932
|
messages.push({ role: "user", content: trimmed });
|
|
1566
933
|
|
|
934
|
+
// Slot-filling: detect intent and fill missing parameters interactively
|
|
935
|
+
try {
|
|
936
|
+
const { CLISlotFiller } = await import("../lib/slot-filler.js");
|
|
937
|
+
const intent = CLISlotFiller.detectIntent(trimmed);
|
|
938
|
+
if (intent) {
|
|
939
|
+
const defs = CLISlotFiller.getSlotDefinitions(intent.type);
|
|
940
|
+
const missing = defs.required.filter((s) => !intent.entities[s]);
|
|
941
|
+
if (missing.length > 0) {
|
|
942
|
+
const { TerminalInteractionAdapter } =
|
|
943
|
+
await import("../lib/interaction-adapter.js");
|
|
944
|
+
const interaction = new TerminalInteractionAdapter();
|
|
945
|
+
const filler = new CLISlotFiller({ interaction });
|
|
946
|
+
const result = await filler.fillSlots(intent, {
|
|
947
|
+
cwd: process.cwd(),
|
|
948
|
+
});
|
|
949
|
+
if (result.filledSlots.length > 0) {
|
|
950
|
+
const parts = Object.entries(result.entities)
|
|
951
|
+
.filter(([, v]) => v)
|
|
952
|
+
.map(([k, v]) => `${k}: ${v}`);
|
|
953
|
+
// Append context to the last user message
|
|
954
|
+
const lastMsg = messages[messages.length - 1];
|
|
955
|
+
lastMsg.content += `\n\n[Context — user provided: ${parts.join(", ")}]`;
|
|
956
|
+
logger.info(chalk.gray(`[slot-fill] ${parts.join(", ")}`));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (_err) {
|
|
961
|
+
// Slot-filling failure is non-critical
|
|
962
|
+
}
|
|
963
|
+
|
|
1567
964
|
// Auto-select best model based on task type
|
|
1568
965
|
let activeModel = model;
|
|
1569
966
|
const taskDetection = detectTaskType(trimmed);
|