chainlesschain 0.42.2 → 0.43.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 +14 -0
- package/package.json +5 -2
- package/src/commands/agent.js +7 -6
- package/src/commands/ask.js +11 -9
- package/src/commands/chat.js +8 -7
- package/src/commands/init.js +1 -1
- package/src/commands/update.js +33 -4
- package/src/lib/agent-coordinator.js +111 -0
- package/src/lib/agent-core.js +167 -2
- package/src/lib/cli-context-engineering.js +48 -15
- package/src/lib/cowork/debate-review-cli.js +12 -2
- package/src/lib/hierarchical-memory.js +186 -68
- package/src/lib/sub-agent-context.js +296 -0
- package/src/lib/sub-agent-registry.js +186 -0
- package/src/lib/ws-session-manager.js +8 -0
- package/src/repl/agent-repl.js +45 -0
package/README.md
CHANGED
|
@@ -9,6 +9,20 @@ npm install -g chainlesschain
|
|
|
9
9
|
chainlesschain setup
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
After installation, three equivalent commands are available:
|
|
13
|
+
|
|
14
|
+
| Command | Description |
|
|
15
|
+
| ---------------- | ----------------------------------------------------------------- |
|
|
16
|
+
| `chainlesschain` | Full name |
|
|
17
|
+
| `cc` | Shortest alias, recommended for daily use |
|
|
18
|
+
| `clc` | ChainLessChain abbreviation, avoids `cc` conflict with C compiler |
|
|
19
|
+
| `clchain` | chainlesschain abbreviation, easy to recognize |
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cc setup # equivalent to: chainlesschain setup
|
|
23
|
+
clchain start # equivalent to: chainlesschain start
|
|
24
|
+
```
|
|
25
|
+
|
|
12
26
|
## Requirements
|
|
13
27
|
|
|
14
28
|
- **Node.js** >= 22.12.0
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chainlesschain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"chainlesschain": "./bin/chainlesschain.js"
|
|
7
|
+
"chainlesschain": "./bin/chainlesschain.js",
|
|
8
|
+
"cc": "./bin/chainlesschain.js",
|
|
9
|
+
"clc": "./bin/chainlesschain.js",
|
|
10
|
+
"clchain": "./bin/chainlesschain.js"
|
|
8
11
|
},
|
|
9
12
|
"main": "src/index.js",
|
|
10
13
|
"scripts": {
|
package/src/commands/agent.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { startAgentRepl } from "../repl/agent-repl.js";
|
|
10
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
10
11
|
|
|
11
12
|
export function registerAgentCommand(program) {
|
|
12
13
|
program
|
|
@@ -15,21 +16,21 @@ export function registerAgentCommand(program) {
|
|
|
15
16
|
.description(
|
|
16
17
|
"Start an agentic AI session (reads/writes files, runs commands)",
|
|
17
18
|
)
|
|
18
|
-
.option("--model <model>", "Model name"
|
|
19
|
+
.option("--model <model>", "Model name")
|
|
19
20
|
.option(
|
|
20
21
|
"--provider <provider>",
|
|
21
22
|
"LLM provider (ollama, openai, volcengine, deepseek, ...)",
|
|
22
|
-
"ollama",
|
|
23
23
|
)
|
|
24
24
|
.option("--base-url <url>", "API base URL")
|
|
25
25
|
.option("--api-key <key>", "API key")
|
|
26
26
|
.option("--session <id>", "Resume a previous agent session")
|
|
27
27
|
.action(async (options) => {
|
|
28
|
+
const config = loadConfig();
|
|
28
29
|
await startAgentRepl({
|
|
29
|
-
model: options.model,
|
|
30
|
-
provider: options.provider,
|
|
31
|
-
baseUrl: options.baseUrl,
|
|
32
|
-
apiKey: options.apiKey,
|
|
30
|
+
model: options.model || config.llm?.model || "qwen2:7b",
|
|
31
|
+
provider: options.provider || config.llm?.provider || "ollama",
|
|
32
|
+
baseUrl: options.baseUrl || config.llm?.baseUrl,
|
|
33
|
+
apiKey: options.apiKey || config.llm?.apiKey,
|
|
33
34
|
sessionId: options.session,
|
|
34
35
|
});
|
|
35
36
|
});
|
package/src/commands/ask.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single-shot AI question command
|
|
3
|
-
* chainlesschain ask "What is..." [--model
|
|
3
|
+
* chainlesschain ask "What is..." [--model] [--provider] [--json]
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { logger } from "../lib/logger.js";
|
|
9
9
|
import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
|
|
10
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Send a single question to an LLM provider
|
|
@@ -84,24 +85,25 @@ export function registerAskCommand(program) {
|
|
|
84
85
|
.command("ask")
|
|
85
86
|
.description("Ask a question to the AI (single-shot)")
|
|
86
87
|
.argument("<question>", "The question to ask")
|
|
87
|
-
.option("--model <model>", "Model name"
|
|
88
|
+
.option("--model <model>", "Model name")
|
|
88
89
|
.option(
|
|
89
90
|
"--provider <provider>",
|
|
90
91
|
"LLM provider (ollama, openai, volcengine, deepseek, ...)",
|
|
91
|
-
"ollama",
|
|
92
92
|
)
|
|
93
93
|
.option("--base-url <url>", "API base URL")
|
|
94
94
|
.option("--api-key <key>", "API key")
|
|
95
95
|
.option("--json", "Output as JSON")
|
|
96
96
|
.action(async (question, options) => {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
const resolvedOptions = {
|
|
99
|
+
model: options.model || config.llm?.model || "qwen2:7b",
|
|
100
|
+
provider: options.provider || config.llm?.provider || "ollama",
|
|
101
|
+
baseUrl: options.baseUrl || config.llm?.baseUrl,
|
|
102
|
+
apiKey: options.apiKey || config.llm?.apiKey,
|
|
103
|
+
};
|
|
97
104
|
const spinner = ora("Thinking...").start();
|
|
98
105
|
try {
|
|
99
|
-
const answer = await queryLLM(question,
|
|
100
|
-
model: options.model,
|
|
101
|
-
provider: options.provider,
|
|
102
|
-
baseUrl: options.baseUrl,
|
|
103
|
-
apiKey: options.apiKey,
|
|
104
|
-
});
|
|
106
|
+
const answer = await queryLLM(question, resolvedOptions);
|
|
105
107
|
|
|
106
108
|
spinner.stop();
|
|
107
109
|
|
package/src/commands/chat.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive AI chat command
|
|
3
|
-
* chainlesschain chat [--model
|
|
3
|
+
* chainlesschain chat [--model] [--provider] [--agent]
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { startChatRepl } from "../repl/chat-repl.js";
|
|
7
7
|
import { startAgentRepl } from "../repl/agent-repl.js";
|
|
8
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
8
9
|
|
|
9
10
|
export function registerChatCommand(program) {
|
|
10
11
|
program
|
|
11
12
|
.command("chat")
|
|
12
13
|
.description("Start an interactive AI chat session")
|
|
13
|
-
.option("--model <model>", "Model name"
|
|
14
|
+
.option("--model <model>", "Model name")
|
|
14
15
|
.option(
|
|
15
16
|
"--provider <provider>",
|
|
16
17
|
"LLM provider (ollama, openai, volcengine, deepseek, ...)",
|
|
17
|
-
"ollama",
|
|
18
18
|
)
|
|
19
19
|
.option("--base-url <url>", "API base URL")
|
|
20
20
|
.option("--api-key <key>", "API key")
|
|
@@ -24,11 +24,12 @@ export function registerChatCommand(program) {
|
|
|
24
24
|
)
|
|
25
25
|
.option("--session <id>", "Resume a previous session (agent mode)")
|
|
26
26
|
.action(async (options) => {
|
|
27
|
+
const config = loadConfig();
|
|
27
28
|
const replOptions = {
|
|
28
|
-
model: options.model,
|
|
29
|
-
provider: options.provider,
|
|
30
|
-
baseUrl: options.baseUrl,
|
|
31
|
-
apiKey: options.apiKey,
|
|
29
|
+
model: options.model || config.llm?.model || "qwen2:7b",
|
|
30
|
+
provider: options.provider || config.llm?.provider || "ollama",
|
|
31
|
+
baseUrl: options.baseUrl || config.llm?.baseUrl,
|
|
32
|
+
apiKey: options.apiKey || config.llm?.apiKey,
|
|
32
33
|
sessionId: options.session,
|
|
33
34
|
};
|
|
34
35
|
|
package/src/commands/init.js
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -8,7 +8,7 @@ import logger from "../lib/logger.js";
|
|
|
8
8
|
|
|
9
9
|
async function selfUpdateCli(targetVersion) {
|
|
10
10
|
if (VERSION === targetVersion) {
|
|
11
|
-
return; // Already at the target version
|
|
11
|
+
return true; // Already at the target version
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
try {
|
|
@@ -17,12 +17,31 @@ async function selfUpdateCli(targetVersion) {
|
|
|
17
17
|
encoding: "utf-8",
|
|
18
18
|
stdio: "pipe",
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// Verify the update actually took effect
|
|
21
|
+
try {
|
|
22
|
+
const newVersion = execSync("chainlesschain --version", {
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
stdio: "pipe",
|
|
25
|
+
}).trim();
|
|
26
|
+
if (newVersion === targetVersion) {
|
|
27
|
+
logger.success(`CLI updated to v${targetVersion}`);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
logger.warn(
|
|
31
|
+
`CLI update ran but version is still ${newVersion}. Please run manually:\n npm install -g chainlesschain@${targetVersion}`,
|
|
32
|
+
);
|
|
33
|
+
return false;
|
|
34
|
+
} catch (_verifyErr) {
|
|
35
|
+
// Cannot verify, assume success
|
|
36
|
+
logger.success(`CLI updated to v${targetVersion}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
21
39
|
} catch (_err) {
|
|
22
40
|
// npm global install may fail due to permissions; guide the user
|
|
23
41
|
logger.warn(
|
|
24
42
|
`CLI self-update failed. Please run manually:\n npm install -g chainlesschain@${targetVersion}`,
|
|
25
43
|
);
|
|
44
|
+
return false;
|
|
26
45
|
}
|
|
27
46
|
}
|
|
28
47
|
|
|
@@ -85,11 +104,21 @@ export function registerUpdateCommand(program) {
|
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
await downloadRelease(result.latestVersion, { force: options.force });
|
|
107
|
+
logger.success("Application already installed");
|
|
88
108
|
|
|
89
109
|
// Self-update the CLI npm package
|
|
90
|
-
await selfUpdateCli(result.latestVersion);
|
|
110
|
+
const cliUpdated = await selfUpdateCli(result.latestVersion);
|
|
91
111
|
|
|
92
|
-
|
|
112
|
+
if (cliUpdated) {
|
|
113
|
+
logger.success(`Updated to v${result.latestVersion}`);
|
|
114
|
+
} else {
|
|
115
|
+
logger.warn(
|
|
116
|
+
`Application binary updated, but CLI version remains at ${VERSION}.`,
|
|
117
|
+
);
|
|
118
|
+
logger.info(
|
|
119
|
+
`To complete the update, run:\n npm install -g chainlesschain@${result.latestVersion}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
93
122
|
logger.info("Restart ChainlessChain to use the new version.");
|
|
94
123
|
} catch (err) {
|
|
95
124
|
if (err.name === "ExitPromptError") {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import crypto from "crypto";
|
|
9
|
+
import { SubAgentContext } from "./sub-agent-context.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Keyword map for agent type detection.
|
|
@@ -271,3 +272,113 @@ export function estimateComplexity(task) {
|
|
|
271
272
|
estimatedSubtasks: Math.max(1, matchedTypes),
|
|
272
273
|
};
|
|
273
274
|
}
|
|
275
|
+
|
|
276
|
+
// ─── Role-based tool whitelist ──────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
export const ROLE_TOOL_WHITELIST = {
|
|
279
|
+
"code-review": ["read_file", "search_files", "list_dir"],
|
|
280
|
+
"code-generation": [
|
|
281
|
+
"read_file",
|
|
282
|
+
"write_file",
|
|
283
|
+
"edit_file",
|
|
284
|
+
"run_shell",
|
|
285
|
+
"search_files",
|
|
286
|
+
"list_dir",
|
|
287
|
+
],
|
|
288
|
+
"data-analysis": [
|
|
289
|
+
"read_file",
|
|
290
|
+
"search_files",
|
|
291
|
+
"list_dir",
|
|
292
|
+
"run_code",
|
|
293
|
+
"run_shell",
|
|
294
|
+
],
|
|
295
|
+
document: ["read_file", "write_file", "search_files", "list_dir"],
|
|
296
|
+
testing: [
|
|
297
|
+
"read_file",
|
|
298
|
+
"write_file",
|
|
299
|
+
"edit_file",
|
|
300
|
+
"run_shell",
|
|
301
|
+
"search_files",
|
|
302
|
+
"list_dir",
|
|
303
|
+
"run_code",
|
|
304
|
+
],
|
|
305
|
+
general: null, // all tools
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Execute a decomposed task using isolated sub-agent contexts.
|
|
310
|
+
* Each subtask gets its own SubAgentContext with role-appropriate tool whitelist.
|
|
311
|
+
*
|
|
312
|
+
* @param {{ taskId: string, subtasks: Array }} decomposition - From decomposeTask()
|
|
313
|
+
* @param {object} [options]
|
|
314
|
+
* @param {string} [options.cwd] - Working directory
|
|
315
|
+
* @param {object} [options.db] - Database instance
|
|
316
|
+
* @param {object} [options.llmOptions] - LLM provider options
|
|
317
|
+
* @param {string} [options.parentContext] - Condensed context from parent
|
|
318
|
+
* @returns {Promise<{ taskId: string, status: string, results: Array, summary: string }>}
|
|
319
|
+
*/
|
|
320
|
+
export async function executeDecomposedTask(decomposition, options = {}) {
|
|
321
|
+
const { subtasks } = decomposition;
|
|
322
|
+
if (!subtasks || subtasks.length === 0) {
|
|
323
|
+
return {
|
|
324
|
+
taskId: decomposition.taskId,
|
|
325
|
+
status: "empty",
|
|
326
|
+
results: [],
|
|
327
|
+
summary: "No subtasks to execute",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const maxConcurrency = options.maxConcurrency || 3;
|
|
332
|
+
|
|
333
|
+
// Run subtasks in parallel batches with concurrency limit
|
|
334
|
+
const results = [];
|
|
335
|
+
for (let i = 0; i < subtasks.length; i += maxConcurrency) {
|
|
336
|
+
const batch = subtasks.slice(i, i + maxConcurrency);
|
|
337
|
+
const batchPromises = batch.map(async (subtask) => {
|
|
338
|
+
const allowedTools = ROLE_TOOL_WHITELIST[subtask.agentType] || null;
|
|
339
|
+
|
|
340
|
+
const subCtx = SubAgentContext.create({
|
|
341
|
+
role: subtask.agentType,
|
|
342
|
+
task: subtask.description,
|
|
343
|
+
inheritedContext: options.parentContext || null,
|
|
344
|
+
allowedTools,
|
|
345
|
+
cwd: options.cwd || process.cwd(),
|
|
346
|
+
db: options.db || null,
|
|
347
|
+
llmOptions: options.llmOptions || {},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const result = await subCtx.run(subtask.description);
|
|
352
|
+
subtask.status = "completed";
|
|
353
|
+
subtask.result = result.summary;
|
|
354
|
+
return {
|
|
355
|
+
id: subtask.id,
|
|
356
|
+
agentType: subtask.agentType,
|
|
357
|
+
status: "completed",
|
|
358
|
+
summary: result.summary,
|
|
359
|
+
toolsUsed: result.toolsUsed,
|
|
360
|
+
};
|
|
361
|
+
} catch (err) {
|
|
362
|
+
subtask.status = "failed";
|
|
363
|
+
subtask.result = err.message;
|
|
364
|
+
return {
|
|
365
|
+
id: subtask.id,
|
|
366
|
+
agentType: subtask.agentType,
|
|
367
|
+
status: "failed",
|
|
368
|
+
error: err.message,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const batchResults = await Promise.all(batchPromises);
|
|
374
|
+
results.push(...batchResults);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const aggregated = aggregateResults(subtasks);
|
|
378
|
+
return {
|
|
379
|
+
taskId: decomposition.taskId,
|
|
380
|
+
status: aggregated.status,
|
|
381
|
+
results,
|
|
382
|
+
summary: aggregated.summary,
|
|
383
|
+
};
|
|
384
|
+
}
|
package/src/lib/agent-core.js
CHANGED
|
@@ -23,6 +23,7 @@ import { CLISkillLoader } from "./skill-loader.js";
|
|
|
23
23
|
import { executeHooks, HookEvents } from "./hook-manager.js";
|
|
24
24
|
import { detectPython } from "./cli-anything-bridge.js";
|
|
25
25
|
import { findProjectRoot, loadProjectConfig } from "./project-detector.js";
|
|
26
|
+
import { SubAgentContext } from "./sub-agent-context.js";
|
|
26
27
|
|
|
27
28
|
// ─── Tool definitions ────────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -212,6 +213,40 @@ export const AGENT_TOOLS = [
|
|
|
212
213
|
},
|
|
213
214
|
},
|
|
214
215
|
},
|
|
216
|
+
{
|
|
217
|
+
type: "function",
|
|
218
|
+
function: {
|
|
219
|
+
name: "spawn_sub_agent",
|
|
220
|
+
description:
|
|
221
|
+
"Spawn an isolated sub-agent to handle a subtask. The sub-agent has its own context and message history, and only returns a summary result. Use this for tasks that benefit from focused, independent execution (e.g. code review, summarization, translation).",
|
|
222
|
+
parameters: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
role: {
|
|
226
|
+
type: "string",
|
|
227
|
+
description:
|
|
228
|
+
"Sub-agent role (e.g. code-review, summarizer, translator, debugger)",
|
|
229
|
+
},
|
|
230
|
+
task: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "Task description for the sub-agent",
|
|
233
|
+
},
|
|
234
|
+
context: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description:
|
|
237
|
+
"Optional condensed context from the parent agent to pass to the sub-agent",
|
|
238
|
+
},
|
|
239
|
+
tools: {
|
|
240
|
+
type: "array",
|
|
241
|
+
items: { type: "string" },
|
|
242
|
+
description:
|
|
243
|
+
'Optional tool whitelist for the sub-agent (e.g. ["read_file", "search_files"]). If omitted, all tools are available.',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ["role", "task"],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
215
250
|
];
|
|
216
251
|
|
|
217
252
|
// ─── Shared skill loader ──────────────────────────────────────────────────
|
|
@@ -326,6 +361,16 @@ When the user's problem involves data processing, calculations, file operations,
|
|
|
326
361
|
|
|
327
362
|
You are not just a chatbot — you are a capable coding agent. Think step by step, write code when needed, and deliver real results.
|
|
328
363
|
|
|
364
|
+
## Sub-Agent Isolation
|
|
365
|
+
When a task involves multiple distinct roles (e.g. code review + code generation), or when you need
|
|
366
|
+
focused analysis without polluting your current context, use the spawn_sub_agent tool. Examples:
|
|
367
|
+
- Code review as a separate perspective while you're implementing
|
|
368
|
+
- Summarizing a large file before incorporating it into your response
|
|
369
|
+
- Running a focused analysis (security, performance) on specific code
|
|
370
|
+
- Translating or reformatting content independently
|
|
371
|
+
The sub-agent has its own message history and only returns a summary — your context stays clean.
|
|
372
|
+
Do NOT spawn sub-agents for trivial tasks that you can handle directly.
|
|
373
|
+
|
|
329
374
|
## Environment
|
|
330
375
|
${envLines.join("\n")}
|
|
331
376
|
|
|
@@ -512,7 +557,11 @@ export async function executeTool(name, args, context = {}) {
|
|
|
512
557
|
|
|
513
558
|
let toolResult;
|
|
514
559
|
try {
|
|
515
|
-
toolResult = await executeToolInner(name, args, {
|
|
560
|
+
toolResult = await executeToolInner(name, args, {
|
|
561
|
+
skillLoader,
|
|
562
|
+
cwd,
|
|
563
|
+
parentMessages: context.parentMessages,
|
|
564
|
+
});
|
|
516
565
|
} catch (err) {
|
|
517
566
|
if (hookDb) {
|
|
518
567
|
try {
|
|
@@ -550,7 +599,11 @@ export async function executeTool(name, args, context = {}) {
|
|
|
550
599
|
/**
|
|
551
600
|
* Inner tool execution — no hooks, no plan-mode checks.
|
|
552
601
|
*/
|
|
553
|
-
async function executeToolInner(
|
|
602
|
+
async function executeToolInner(
|
|
603
|
+
name,
|
|
604
|
+
args,
|
|
605
|
+
{ skillLoader, cwd, parentMessages },
|
|
606
|
+
) {
|
|
554
607
|
switch (name) {
|
|
555
608
|
case "read_file": {
|
|
556
609
|
const filePath = path.resolve(cwd, args.path);
|
|
@@ -613,6 +666,10 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
|
|
|
613
666
|
return _executeRunCode(args, cwd);
|
|
614
667
|
}
|
|
615
668
|
|
|
669
|
+
case "spawn_sub_agent": {
|
|
670
|
+
return _executeSpawnSubAgent(args, { skillLoader, cwd, parentMessages });
|
|
671
|
+
}
|
|
672
|
+
|
|
616
673
|
case "search_files": {
|
|
617
674
|
const dir = args.directory ? path.resolve(cwd, args.directory) : cwd;
|
|
618
675
|
try {
|
|
@@ -676,6 +733,31 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
|
|
|
676
733
|
error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
|
|
677
734
|
};
|
|
678
735
|
}
|
|
736
|
+
|
|
737
|
+
// Check if skill requests isolation (via SKILL.md frontmatter)
|
|
738
|
+
const skillIsolation = match.isolation === true;
|
|
739
|
+
if (skillIsolation) {
|
|
740
|
+
// Run skill through isolated sub-agent context
|
|
741
|
+
const subCtx = SubAgentContext.create({
|
|
742
|
+
role: `skill-${args.skill_name}`,
|
|
743
|
+
task: `Execute the "${args.skill_name}" skill with input: ${(args.input || "").substring(0, 200)}`,
|
|
744
|
+
allowedTools: ["read_file", "search_files", "list_dir"],
|
|
745
|
+
cwd,
|
|
746
|
+
});
|
|
747
|
+
try {
|
|
748
|
+
const result = await subCtx.run(args.input);
|
|
749
|
+
return {
|
|
750
|
+
success: true,
|
|
751
|
+
isolated: true,
|
|
752
|
+
skill: args.skill_name,
|
|
753
|
+
summary: result.summary,
|
|
754
|
+
toolsUsed: result.toolsUsed,
|
|
755
|
+
};
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return { error: `Isolated skill execution failed: ${err.message}` };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
679
761
|
try {
|
|
680
762
|
const handlerPath = path.join(match.skillDir, "handler.js");
|
|
681
763
|
const imported = await import(
|
|
@@ -953,6 +1035,86 @@ async function _executeRunCode(args, cwd) {
|
|
|
953
1035
|
}
|
|
954
1036
|
}
|
|
955
1037
|
|
|
1038
|
+
// ─── spawn_sub_agent implementation ──────────────────────────────────────
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Execute a spawn_sub_agent tool call.
|
|
1042
|
+
* Creates an isolated SubAgentContext, runs it, and returns only the summary.
|
|
1043
|
+
*
|
|
1044
|
+
* @param {object} args - { role, task, context?, tools? }
|
|
1045
|
+
* @param {object} ctx - { skillLoader, cwd }
|
|
1046
|
+
* @returns {Promise<object>}
|
|
1047
|
+
*/
|
|
1048
|
+
async function _executeSpawnSubAgent(args, ctx) {
|
|
1049
|
+
const { role, task, context: inheritedContext, tools: allowedTools } = args;
|
|
1050
|
+
|
|
1051
|
+
if (!role || !task) {
|
|
1052
|
+
return { error: "Both 'role' and 'task' are required for spawn_sub_agent" };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Auto-condense parent context if caller didn't provide explicit context
|
|
1056
|
+
let resolvedContext = inheritedContext || null;
|
|
1057
|
+
if (!resolvedContext && Array.isArray(ctx.parentMessages)) {
|
|
1058
|
+
const recentMsgs = ctx.parentMessages
|
|
1059
|
+
.filter((m) => m.role === "assistant" && typeof m.content === "string")
|
|
1060
|
+
.slice(-3)
|
|
1061
|
+
.map((m) => m.content.substring(0, 200));
|
|
1062
|
+
if (recentMsgs.length > 0) {
|
|
1063
|
+
resolvedContext = recentMsgs.join("\n---\n");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const subCtx = SubAgentContext.create({
|
|
1068
|
+
role,
|
|
1069
|
+
task,
|
|
1070
|
+
inheritedContext: resolvedContext,
|
|
1071
|
+
allowedTools: allowedTools || null,
|
|
1072
|
+
cwd: ctx.cwd,
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
// Notify registry if available
|
|
1077
|
+
const { SubAgentRegistry } = await import("./sub-agent-registry.js").catch(
|
|
1078
|
+
() => ({ SubAgentRegistry: null }),
|
|
1079
|
+
);
|
|
1080
|
+
if (SubAgentRegistry) {
|
|
1081
|
+
try {
|
|
1082
|
+
SubAgentRegistry.getInstance().register(subCtx);
|
|
1083
|
+
} catch (_err) {
|
|
1084
|
+
// Registry not available — non-critical
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const result = await subCtx.run(task);
|
|
1089
|
+
|
|
1090
|
+
// Complete in registry
|
|
1091
|
+
if (SubAgentRegistry) {
|
|
1092
|
+
try {
|
|
1093
|
+
SubAgentRegistry.getInstance().complete(subCtx.id, result);
|
|
1094
|
+
} catch (_err) {
|
|
1095
|
+
// Non-critical
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return {
|
|
1100
|
+
success: true,
|
|
1101
|
+
subAgentId: subCtx.id,
|
|
1102
|
+
role: subCtx.role,
|
|
1103
|
+
summary: result.summary,
|
|
1104
|
+
toolsUsed: result.toolsUsed,
|
|
1105
|
+
iterationCount: result.iterationCount,
|
|
1106
|
+
artifactCount: result.artifacts.length,
|
|
1107
|
+
};
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
subCtx.forceComplete(err.message);
|
|
1110
|
+
return {
|
|
1111
|
+
error: `Sub-agent failed: ${err.message}`,
|
|
1112
|
+
subAgentId: subCtx.id,
|
|
1113
|
+
role: subCtx.role,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
956
1118
|
// ─── LLM chat with tools ─────────────────────────────────────────────────
|
|
957
1119
|
|
|
958
1120
|
/**
|
|
@@ -1157,6 +1319,7 @@ export async function* agentLoop(messages, options) {
|
|
|
1157
1319
|
hookDb: options.hookDb || null,
|
|
1158
1320
|
skillLoader: options.skillLoader || _defaultSkillLoader,
|
|
1159
1321
|
cwd: options.cwd || process.cwd(),
|
|
1322
|
+
parentMessages: messages, // pass parent messages for sub-agent auto-condensation
|
|
1160
1323
|
};
|
|
1161
1324
|
|
|
1162
1325
|
// ── Slot-filling phase ──────────────────────────────────────────────
|
|
@@ -1292,6 +1455,8 @@ export function formatToolArgs(name, args) {
|
|
|
1292
1455
|
return args.category || args.query || "all";
|
|
1293
1456
|
case "run_code":
|
|
1294
1457
|
return `${args.language} (${(args.code || "").length} chars)`;
|
|
1458
|
+
case "spawn_sub_agent":
|
|
1459
|
+
return `[${args.role}] ${(args.task || "").substring(0, 60)}`;
|
|
1295
1460
|
default:
|
|
1296
1461
|
return JSON.stringify(args).substring(0, 60);
|
|
1297
1462
|
}
|