@cuylabs/agent-core 0.3.0 → 0.4.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 +162 -36
- package/dist/{index-QR704uRr.d.ts → index-BlSTfS-W.d.ts} +4 -6
- package/dist/index.d.ts +2432 -1037
- package/dist/index.js +1571 -61
- package/dist/tool/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1175,18 +1175,18 @@ var TurnChangeTracker = class {
|
|
|
1175
1175
|
if (this.gitDetected !== null) {
|
|
1176
1176
|
return this.gitDetected;
|
|
1177
1177
|
}
|
|
1178
|
-
return new Promise((
|
|
1178
|
+
return new Promise((resolve6) => {
|
|
1179
1179
|
const proc = spawn("git", ["rev-parse", "--git-dir"], {
|
|
1180
1180
|
cwd: this.config.cwd,
|
|
1181
1181
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1182
1182
|
});
|
|
1183
|
-
proc.on("close", (
|
|
1184
|
-
this.gitDetected =
|
|
1185
|
-
|
|
1183
|
+
proc.on("close", (code2) => {
|
|
1184
|
+
this.gitDetected = code2 === 0;
|
|
1185
|
+
resolve6(this.gitDetected);
|
|
1186
1186
|
});
|
|
1187
1187
|
proc.on("error", () => {
|
|
1188
1188
|
this.gitDetected = false;
|
|
1189
|
-
|
|
1189
|
+
resolve6(false);
|
|
1190
1190
|
});
|
|
1191
1191
|
});
|
|
1192
1192
|
}
|
|
@@ -2655,14 +2655,14 @@ function calculateDelay(attempt, error, config) {
|
|
|
2655
2655
|
return Math.round(cappedDelay);
|
|
2656
2656
|
}
|
|
2657
2657
|
async function sleep(ms, signal) {
|
|
2658
|
-
return new Promise((
|
|
2658
|
+
return new Promise((resolve6, reject) => {
|
|
2659
2659
|
if (signal?.aborted) {
|
|
2660
2660
|
reject(new DOMException("Aborted", "AbortError"));
|
|
2661
2661
|
return;
|
|
2662
2662
|
}
|
|
2663
2663
|
const timeoutId = setTimeout(() => {
|
|
2664
2664
|
cleanup();
|
|
2665
|
-
|
|
2665
|
+
resolve6();
|
|
2666
2666
|
}, ms);
|
|
2667
2667
|
const abortHandler = () => {
|
|
2668
2668
|
clearTimeout(timeoutId);
|
|
@@ -2720,7 +2720,7 @@ function shouldRetry(error, attempt, maxAttempts = DEFAULT_RETRY_CONFIG.maxAttem
|
|
|
2720
2720
|
var OUTPUT_TOKEN_MAX = 32e3;
|
|
2721
2721
|
var LLM;
|
|
2722
2722
|
((LLM2) => {
|
|
2723
|
-
async function buildToolSet(tools, cwd, sessionID, messageID, abort, turnTracker, host) {
|
|
2723
|
+
async function buildToolSet(tools, cwd, sessionID, messageID, abort, turnTracker, host, middleware) {
|
|
2724
2724
|
const toolSet = {};
|
|
2725
2725
|
for (const [id, info] of Object.entries(tools)) {
|
|
2726
2726
|
const initialized = await info.init({ cwd });
|
|
@@ -2737,6 +2737,12 @@ var LLM;
|
|
|
2737
2737
|
host,
|
|
2738
2738
|
turnTracker
|
|
2739
2739
|
};
|
|
2740
|
+
if (middleware?.hasMiddleware) {
|
|
2741
|
+
const decision = await middleware.runBeforeToolCall(id, params, ctx);
|
|
2742
|
+
if (decision.action === "deny") {
|
|
2743
|
+
return decision.reason ?? `Tool call denied: ${id}`;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2740
2746
|
if (turnTracker && initialized.fileOps && shouldCaptureBaseline(initialized.fileOps)) {
|
|
2741
2747
|
const paths = extractFilePathsFromArgs(
|
|
2742
2748
|
params,
|
|
@@ -2747,6 +2753,10 @@ var LLM;
|
|
|
2747
2753
|
}
|
|
2748
2754
|
}
|
|
2749
2755
|
const result = await initialized.execute(params, ctx);
|
|
2756
|
+
if (middleware?.hasMiddleware) {
|
|
2757
|
+
const transformed = await middleware.runAfterToolCall(id, params, result, ctx);
|
|
2758
|
+
return transformed.output;
|
|
2759
|
+
}
|
|
2750
2760
|
return result.output;
|
|
2751
2761
|
}
|
|
2752
2762
|
});
|
|
@@ -2778,7 +2788,8 @@ var LLM;
|
|
|
2778
2788
|
messageID,
|
|
2779
2789
|
input.abort,
|
|
2780
2790
|
input.turnTracker,
|
|
2781
|
-
input.host
|
|
2791
|
+
input.host,
|
|
2792
|
+
input.middleware
|
|
2782
2793
|
);
|
|
2783
2794
|
const allTools = {
|
|
2784
2795
|
...toolSet,
|
|
@@ -3442,11 +3453,18 @@ var explore = {
|
|
|
3442
3453
|
You are in **exploration mode**. Your goal is to thoroughly understand the content.
|
|
3443
3454
|
|
|
3444
3455
|
Guidelines:
|
|
3456
|
+
- **Always start with glob or grep to discover correct paths** \u2014 never assume directory structure
|
|
3445
3457
|
- Use search and read tools extensively to build understanding
|
|
3446
3458
|
- Map out structures, dependencies, and patterns
|
|
3447
3459
|
- Look for conventions, common patterns, and documentation
|
|
3448
3460
|
- DO NOT modify anything \u2014 only read and analyse
|
|
3449
|
-
- Summarise findings clearly with references to specific locations
|
|
3461
|
+
- Summarise findings clearly with references to specific locations
|
|
3462
|
+
|
|
3463
|
+
Error recovery:
|
|
3464
|
+
- If a tool returns an error (ENOENT, not found, etc.), try a different path or approach
|
|
3465
|
+
- Use glob with broader patterns to discover the correct location
|
|
3466
|
+
- Keep going until you have a thorough answer \u2014 do not give up after one failure
|
|
3467
|
+
- You have multiple steps available \u2014 use them all if needed`,
|
|
3450
3468
|
temperature: 0.3,
|
|
3451
3469
|
maxSteps: 30
|
|
3452
3470
|
};
|
|
@@ -3536,12 +3554,61 @@ Guidelines:
|
|
|
3536
3554
|
temperature: 0,
|
|
3537
3555
|
maxSteps: 50
|
|
3538
3556
|
};
|
|
3557
|
+
var code = {
|
|
3558
|
+
name: "code",
|
|
3559
|
+
description: "Full-power implementation mode for writing and modifying code",
|
|
3560
|
+
systemPrompt: `{basePrompt}
|
|
3561
|
+
|
|
3562
|
+
## IMPLEMENTATION MODE
|
|
3563
|
+
|
|
3564
|
+
You are a focused **implementation agent**. Your goal is to complete the assigned task fully and correctly.
|
|
3565
|
+
|
|
3566
|
+
Guidelines:
|
|
3567
|
+
- Read existing code first to understand context and conventions before making changes
|
|
3568
|
+
- Follow the project's existing patterns, naming conventions, and style
|
|
3569
|
+
- Make minimal, targeted changes \u2014 do not refactor unrelated code
|
|
3570
|
+
- Verify your work: re-read modified files, run tests or lint if available
|
|
3571
|
+
- If the task requires multiple files, handle them systematically one at a time
|
|
3572
|
+
|
|
3573
|
+
Error recovery:
|
|
3574
|
+
- If a command fails, read the error output carefully and fix the root cause
|
|
3575
|
+
- If a test fails, read the failure details and iterate until it passes
|
|
3576
|
+
- If you cannot find a file, use glob or grep to discover the correct path
|
|
3577
|
+
- Keep iterating until the task is DONE \u2014 do not stop at a partial solution
|
|
3578
|
+
- You have many steps available \u2014 use them all if needed`,
|
|
3579
|
+
temperature: 0,
|
|
3580
|
+
maxSteps: 50
|
|
3581
|
+
};
|
|
3582
|
+
var watch = {
|
|
3583
|
+
name: "watch",
|
|
3584
|
+
description: "Process monitoring mode for running and watching long commands",
|
|
3585
|
+
allowTools: ["bash*", "exec*", "run*", "read*", "grep*", "glob*", "list*", "find*"],
|
|
3586
|
+
denyTools: ["write*", "edit*", "delete*", "remove*"],
|
|
3587
|
+
systemPrompt: `{basePrompt}
|
|
3588
|
+
|
|
3589
|
+
## WATCH MODE
|
|
3590
|
+
|
|
3591
|
+
You are a **process monitor**. Your goal is to execute a command, observe its output, and report the result.
|
|
3592
|
+
|
|
3593
|
+
Guidelines:
|
|
3594
|
+
- Run the command as given \u2014 do not modify or "improve" it
|
|
3595
|
+
- Wait for completion and capture the full output
|
|
3596
|
+
- Parse the output for success/failure status, error counts, warnings
|
|
3597
|
+
- Report a clear summary: what ran, whether it passed, key details
|
|
3598
|
+
- If the process fails, include the relevant error output verbatim
|
|
3599
|
+
- Do NOT attempt to fix issues \u2014 only observe and report`,
|
|
3600
|
+
temperature: 0,
|
|
3601
|
+
maxSteps: 10,
|
|
3602
|
+
reasoningLevel: "low"
|
|
3603
|
+
};
|
|
3539
3604
|
var Presets = {
|
|
3540
3605
|
explore,
|
|
3541
3606
|
plan,
|
|
3542
3607
|
review,
|
|
3543
3608
|
quick,
|
|
3544
|
-
careful
|
|
3609
|
+
careful,
|
|
3610
|
+
code,
|
|
3611
|
+
watch
|
|
3545
3612
|
};
|
|
3546
3613
|
function createPreset(options) {
|
|
3547
3614
|
return {
|
|
@@ -3597,7 +3664,8 @@ var TEMPLATE_ANTHROPIC = `You are a capable AI assistant with access to tools.
|
|
|
3597
3664
|
- Use the tools available to you to accomplish tasks
|
|
3598
3665
|
- Choose the most appropriate tool for each step
|
|
3599
3666
|
- Review tool output before proceeding to the next step
|
|
3600
|
-
- If a tool fails, try an alternative approach
|
|
3667
|
+
- If a tool fails, try an alternative approach \u2014 do not give up after one error
|
|
3668
|
+
- Keep iterating until the task is fully resolved or all options are exhausted
|
|
3601
3669
|
</tool-usage>
|
|
3602
3670
|
|
|
3603
3671
|
<communication>
|
|
@@ -3618,8 +3686,9 @@ var TEMPLATE_OPENAI = `You are a capable AI assistant with access to tools.
|
|
|
3618
3686
|
## Tool Usage
|
|
3619
3687
|
1. Use the most appropriate tool for each step
|
|
3620
3688
|
2. Review tool output before proceeding
|
|
3621
|
-
3. If a tool fails, try an alternative approach
|
|
3689
|
+
3. If a tool fails, try an alternative approach \u2014 do not give up after one error
|
|
3622
3690
|
4. Combine tools effectively to accomplish complex tasks
|
|
3691
|
+
5. Keep iterating until the task is fully resolved or all options are exhausted
|
|
3623
3692
|
|
|
3624
3693
|
## Communication
|
|
3625
3694
|
- Be direct and concise
|
|
@@ -3638,7 +3707,8 @@ var TEMPLATE_GOOGLE = `You are a capable AI assistant with access to tools.
|
|
|
3638
3707
|
**Tool Usage:**
|
|
3639
3708
|
- Choose the most appropriate tool for each step
|
|
3640
3709
|
- Review tool output before proceeding
|
|
3641
|
-
- If a tool fails, try an alternative approach
|
|
3710
|
+
- If a tool fails, try an alternative approach \u2014 do not give up after one error
|
|
3711
|
+
- Keep iterating until the task is fully resolved or all options are exhausted
|
|
3642
3712
|
|
|
3643
3713
|
**Communication:**
|
|
3644
3714
|
- Be concise and direct
|
|
@@ -3651,6 +3721,8 @@ Principles:
|
|
|
3651
3721
|
- Break complex tasks into smaller steps
|
|
3652
3722
|
- Gather context and verify assumptions
|
|
3653
3723
|
- Use tools effectively \u2014 choose the right one for each step
|
|
3724
|
+
- If a tool fails, try an alternative \u2014 do not give up after one error
|
|
3725
|
+
- Keep iterating until the task is fully resolved
|
|
3654
3726
|
- Verify results after each action
|
|
3655
3727
|
|
|
3656
3728
|
Communication:
|
|
@@ -3664,6 +3736,8 @@ Guidelines:
|
|
|
3664
3736
|
- Break complex tasks into smaller, verifiable steps
|
|
3665
3737
|
- Gather context and information before making decisions
|
|
3666
3738
|
- Use the most appropriate tool for each step
|
|
3739
|
+
- If a tool fails, try an alternative \u2014 do not give up after one error
|
|
3740
|
+
- Keep iterating until the task is fully resolved or all options are exhausted
|
|
3667
3741
|
- Verify results after each action
|
|
3668
3742
|
- Be concise and direct in communication
|
|
3669
3743
|
- Proactively mention potential issues or risks`;
|
|
@@ -3869,6 +3943,577 @@ function formatInstructions(files, basePath) {
|
|
|
3869
3943
|
return blocks.join("\n");
|
|
3870
3944
|
}
|
|
3871
3945
|
|
|
3946
|
+
// src/skill/discovery.ts
|
|
3947
|
+
import { stat as stat5, readdir as readdir3, access as access2 } from "fs/promises";
|
|
3948
|
+
import { join as join5, resolve as resolve4, dirname as dirname3, sep as sep2 } from "path";
|
|
3949
|
+
import { homedir as homedir4 } from "os";
|
|
3950
|
+
|
|
3951
|
+
// src/skill/loader.ts
|
|
3952
|
+
import { readFile as readFile4, stat as stat4, readdir as readdir2 } from "fs/promises";
|
|
3953
|
+
import { join as join4, relative as relative2, extname } from "path";
|
|
3954
|
+
var SKILL_FILENAME = "SKILL.md";
|
|
3955
|
+
var DEFAULT_SKILL_MAX_SIZE = 102400;
|
|
3956
|
+
var RESOURCE_DIRS = {
|
|
3957
|
+
scripts: "script",
|
|
3958
|
+
references: "reference",
|
|
3959
|
+
assets: "asset",
|
|
3960
|
+
examples: "example"
|
|
3961
|
+
};
|
|
3962
|
+
function parseFrontmatter(raw) {
|
|
3963
|
+
const trimmed = raw.trimStart();
|
|
3964
|
+
if (!trimmed.startsWith("---")) {
|
|
3965
|
+
return { data: {}, body: raw };
|
|
3966
|
+
}
|
|
3967
|
+
const endIndex = trimmed.indexOf("\n---", 3);
|
|
3968
|
+
if (endIndex === -1) {
|
|
3969
|
+
return { data: {}, body: raw };
|
|
3970
|
+
}
|
|
3971
|
+
const yamlBlock = trimmed.slice(4, endIndex).trim();
|
|
3972
|
+
const body = trimmed.slice(endIndex + 4).trim();
|
|
3973
|
+
const data = {};
|
|
3974
|
+
const lines = yamlBlock.split("\n");
|
|
3975
|
+
let currentKey = null;
|
|
3976
|
+
let continuationValue = "";
|
|
3977
|
+
for (const line of lines) {
|
|
3978
|
+
if (line.trimStart().startsWith("#") || line.trim() === "") continue;
|
|
3979
|
+
const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)/);
|
|
3980
|
+
if (kvMatch) {
|
|
3981
|
+
if (currentKey && continuationValue) {
|
|
3982
|
+
data[currentKey] = parseFrontmatterValue(continuationValue.trim());
|
|
3983
|
+
continuationValue = "";
|
|
3984
|
+
}
|
|
3985
|
+
const key = kvMatch[1];
|
|
3986
|
+
const rawValue = kvMatch[2].trim();
|
|
3987
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
3988
|
+
data[key] = rawValue.slice(1, -1).split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3989
|
+
currentKey = null;
|
|
3990
|
+
} else if (rawValue === "" || rawValue === "|" || rawValue === ">") {
|
|
3991
|
+
currentKey = key;
|
|
3992
|
+
continuationValue = "";
|
|
3993
|
+
} else {
|
|
3994
|
+
data[key] = parseFrontmatterValue(rawValue);
|
|
3995
|
+
currentKey = null;
|
|
3996
|
+
}
|
|
3997
|
+
} else if (currentKey) {
|
|
3998
|
+
continuationValue += (continuationValue ? "\n" : "") + line.trim();
|
|
3999
|
+
} else if (line.trimStart().startsWith("- ")) {
|
|
4000
|
+
const lastKey = Object.keys(data).length > 0 ? Object.keys(data)[Object.keys(data).length - 1] : null;
|
|
4001
|
+
if (lastKey) {
|
|
4002
|
+
const existing = data[lastKey];
|
|
4003
|
+
const item = line.trimStart().slice(2).trim();
|
|
4004
|
+
if (Array.isArray(existing)) {
|
|
4005
|
+
existing.push(item);
|
|
4006
|
+
} else {
|
|
4007
|
+
data[lastKey] = [item];
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
if (currentKey && continuationValue) {
|
|
4013
|
+
data[currentKey] = parseFrontmatterValue(continuationValue.trim());
|
|
4014
|
+
}
|
|
4015
|
+
return { data, body };
|
|
4016
|
+
}
|
|
4017
|
+
function parseFrontmatterValue(raw) {
|
|
4018
|
+
if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
|
|
4019
|
+
return raw.slice(1, -1);
|
|
4020
|
+
}
|
|
4021
|
+
if (raw === "true") return true;
|
|
4022
|
+
if (raw === "false") return false;
|
|
4023
|
+
if (/^\d+(\.\d+)?$/.test(raw)) return Number(raw);
|
|
4024
|
+
if (raw === "null" || raw === "~") return null;
|
|
4025
|
+
return raw;
|
|
4026
|
+
}
|
|
4027
|
+
async function loadSkillMetadata(filePath, scope, source, maxSize = DEFAULT_SKILL_MAX_SIZE) {
|
|
4028
|
+
try {
|
|
4029
|
+
const info = await stat4(filePath);
|
|
4030
|
+
if (!info.isFile() || info.size > maxSize) return null;
|
|
4031
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
4032
|
+
const { data } = parseFrontmatter(raw);
|
|
4033
|
+
const name = typeof data.name === "string" ? data.name.trim() : null;
|
|
4034
|
+
const description = typeof data.description === "string" ? data.description.trim() : null;
|
|
4035
|
+
if (!name || !description) return null;
|
|
4036
|
+
const baseDir = join4(filePath, "..");
|
|
4037
|
+
return {
|
|
4038
|
+
name,
|
|
4039
|
+
description,
|
|
4040
|
+
version: typeof data.version === "string" ? data.version : void 0,
|
|
4041
|
+
scope,
|
|
4042
|
+
source,
|
|
4043
|
+
filePath,
|
|
4044
|
+
baseDir,
|
|
4045
|
+
tags: Array.isArray(data.tags) ? data.tags.map(String) : void 0,
|
|
4046
|
+
dependencies: Array.isArray(data.dependencies) ? data.dependencies.map(String) : void 0
|
|
4047
|
+
};
|
|
4048
|
+
} catch {
|
|
4049
|
+
return null;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
async function loadSkillContent(metadata) {
|
|
4053
|
+
const raw = await readFile4(metadata.filePath, "utf-8");
|
|
4054
|
+
const { body } = parseFrontmatter(raw);
|
|
4055
|
+
const resources = await scanSkillResources(metadata.baseDir);
|
|
4056
|
+
return {
|
|
4057
|
+
...metadata,
|
|
4058
|
+
body,
|
|
4059
|
+
resources
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
4062
|
+
async function loadResourceContent(resource) {
|
|
4063
|
+
return readFile4(resource.absolutePath, "utf-8");
|
|
4064
|
+
}
|
|
4065
|
+
async function scanSkillResources(baseDir) {
|
|
4066
|
+
const resources = [];
|
|
4067
|
+
for (const [dirName, resourceType] of Object.entries(RESOURCE_DIRS)) {
|
|
4068
|
+
const dirPath = join4(baseDir, dirName);
|
|
4069
|
+
try {
|
|
4070
|
+
const info = await stat4(dirPath);
|
|
4071
|
+
if (!info.isDirectory()) continue;
|
|
4072
|
+
} catch {
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
await collectFiles(dirPath, baseDir, resourceType, resources);
|
|
4076
|
+
}
|
|
4077
|
+
return resources;
|
|
4078
|
+
}
|
|
4079
|
+
async function collectFiles(dirPath, baseDir, type, resources, maxDepth = 3, currentDepth = 0) {
|
|
4080
|
+
if (currentDepth >= maxDepth) return;
|
|
4081
|
+
try {
|
|
4082
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
4083
|
+
for (const entry of entries) {
|
|
4084
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
4085
|
+
const fullPath = join4(dirPath, entry.name);
|
|
4086
|
+
if (entry.isFile()) {
|
|
4087
|
+
resources.push({
|
|
4088
|
+
relativePath: relative2(baseDir, fullPath),
|
|
4089
|
+
type,
|
|
4090
|
+
absolutePath: fullPath
|
|
4091
|
+
});
|
|
4092
|
+
} else if (entry.isDirectory()) {
|
|
4093
|
+
await collectFiles(fullPath, baseDir, type, resources, maxDepth, currentDepth + 1);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
function inferResourceType(filePath) {
|
|
4100
|
+
const ext = extname(filePath).toLowerCase();
|
|
4101
|
+
if ([".sh", ".bash", ".py", ".rb", ".js", ".ts"].includes(ext)) {
|
|
4102
|
+
return "script";
|
|
4103
|
+
}
|
|
4104
|
+
if ([".md", ".txt", ".rst", ".adoc"].includes(ext)) {
|
|
4105
|
+
return "reference";
|
|
4106
|
+
}
|
|
4107
|
+
if ([".json", ".yaml", ".yml", ".toml", ".xml", ".html", ".css"].includes(ext)) {
|
|
4108
|
+
return "asset";
|
|
4109
|
+
}
|
|
4110
|
+
return "reference";
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// src/skill/discovery.ts
|
|
4114
|
+
var DEFAULT_EXTERNAL_DIRS = [".agents", ".claude"];
|
|
4115
|
+
var DEFAULT_MAX_SCAN_DEPTH = 4;
|
|
4116
|
+
var MAX_SKILLS_PER_ROOT = 500;
|
|
4117
|
+
async function discoverSkills(cwd, config) {
|
|
4118
|
+
const startTime = Date.now();
|
|
4119
|
+
const resolvedCwd = resolve4(cwd);
|
|
4120
|
+
const errors = [];
|
|
4121
|
+
let dirsScanned = 0;
|
|
4122
|
+
const externalDirs = config?.externalDirs ?? DEFAULT_EXTERNAL_DIRS;
|
|
4123
|
+
const maxDepth = config?.maxScanDepth ?? DEFAULT_MAX_SCAN_DEPTH;
|
|
4124
|
+
const maxFileSize = config?.maxFileSize ?? DEFAULT_SKILL_MAX_SIZE;
|
|
4125
|
+
const roots = [];
|
|
4126
|
+
if (config?.roots) {
|
|
4127
|
+
for (const root of config.roots) {
|
|
4128
|
+
const absRoot = resolve4(resolvedCwd, root);
|
|
4129
|
+
roots.push({ path: absRoot, scope: "global", source: { type: "local", root: absRoot } });
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
const home = homedir4();
|
|
4133
|
+
for (const dir of externalDirs) {
|
|
4134
|
+
const skillsDir = join5(home, dir, "skills");
|
|
4135
|
+
if (await dirExists(skillsDir)) {
|
|
4136
|
+
roots.push({
|
|
4137
|
+
path: skillsDir,
|
|
4138
|
+
scope: "user",
|
|
4139
|
+
source: { type: "local", root: join5(home, dir) }
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
const gitRoot = await findGitRoot2(resolvedCwd);
|
|
4144
|
+
if (gitRoot) {
|
|
4145
|
+
const dirsInPath = dirsBetween(gitRoot, resolvedCwd);
|
|
4146
|
+
for (const dir of dirsInPath) {
|
|
4147
|
+
for (const extDir of externalDirs) {
|
|
4148
|
+
const skillsDir = join5(dir, extDir, "skills");
|
|
4149
|
+
if (await dirExists(skillsDir)) {
|
|
4150
|
+
roots.push({
|
|
4151
|
+
path: skillsDir,
|
|
4152
|
+
scope: "project",
|
|
4153
|
+
source: { type: "local", root: join5(dir, extDir) }
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
} else {
|
|
4159
|
+
for (const extDir of externalDirs) {
|
|
4160
|
+
const skillsDir = join5(resolvedCwd, extDir, "skills");
|
|
4161
|
+
if (await dirExists(skillsDir)) {
|
|
4162
|
+
roots.push({
|
|
4163
|
+
path: skillsDir,
|
|
4164
|
+
scope: "project",
|
|
4165
|
+
source: { type: "local", root: join5(resolvedCwd, extDir) }
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
const allSkills = [];
|
|
4171
|
+
for (const root of roots) {
|
|
4172
|
+
const result = await scanRoot(root, maxDepth, maxFileSize);
|
|
4173
|
+
allSkills.push(...result.skills);
|
|
4174
|
+
errors.push(...result.errors);
|
|
4175
|
+
dirsScanned += result.dirsScanned;
|
|
4176
|
+
}
|
|
4177
|
+
const deduped = deduplicateSkills(allSkills);
|
|
4178
|
+
return {
|
|
4179
|
+
skills: deduped,
|
|
4180
|
+
errors,
|
|
4181
|
+
dirsScanned,
|
|
4182
|
+
durationMs: Date.now() - startTime
|
|
4183
|
+
};
|
|
4184
|
+
}
|
|
4185
|
+
async function scanRoot(root, maxDepth, maxFileSize) {
|
|
4186
|
+
const skills = [];
|
|
4187
|
+
const errors = [];
|
|
4188
|
+
let dirsScanned = 0;
|
|
4189
|
+
const queue = [[root.path, 0]];
|
|
4190
|
+
while (queue.length > 0 && skills.length < MAX_SKILLS_PER_ROOT) {
|
|
4191
|
+
const [dirPath, depth] = queue.shift();
|
|
4192
|
+
if (depth > maxDepth) continue;
|
|
4193
|
+
dirsScanned++;
|
|
4194
|
+
try {
|
|
4195
|
+
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
4196
|
+
const hasSkillFile = entries.some(
|
|
4197
|
+
(e) => e.isFile() && e.name === SKILL_FILENAME
|
|
4198
|
+
);
|
|
4199
|
+
if (hasSkillFile) {
|
|
4200
|
+
const filePath = join5(dirPath, SKILL_FILENAME);
|
|
4201
|
+
const metadata = await loadSkillMetadata(
|
|
4202
|
+
filePath,
|
|
4203
|
+
root.scope,
|
|
4204
|
+
root.source,
|
|
4205
|
+
maxFileSize
|
|
4206
|
+
);
|
|
4207
|
+
if (metadata) {
|
|
4208
|
+
skills.push(metadata);
|
|
4209
|
+
} else {
|
|
4210
|
+
errors.push({
|
|
4211
|
+
path: filePath,
|
|
4212
|
+
reason: "Invalid or incomplete frontmatter (name and description required)"
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
for (const entry of entries) {
|
|
4218
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
|
|
4219
|
+
continue;
|
|
4220
|
+
}
|
|
4221
|
+
queue.push([join5(dirPath, entry.name), depth + 1]);
|
|
4222
|
+
}
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
errors.push({
|
|
4225
|
+
path: dirPath,
|
|
4226
|
+
reason: `Cannot read directory: ${err instanceof Error ? err.message : String(err)}`
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
return { skills, errors, dirsScanned };
|
|
4231
|
+
}
|
|
4232
|
+
var SCOPE_PRIORITY = {
|
|
4233
|
+
project: 0,
|
|
4234
|
+
user: 1,
|
|
4235
|
+
global: 2,
|
|
4236
|
+
builtin: 3
|
|
4237
|
+
};
|
|
4238
|
+
function deduplicateSkills(skills) {
|
|
4239
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4240
|
+
for (const skill of skills) {
|
|
4241
|
+
const existing = byName.get(skill.name);
|
|
4242
|
+
if (!existing) {
|
|
4243
|
+
byName.set(skill.name, skill);
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
const existingPriority = SCOPE_PRIORITY[existing.scope];
|
|
4247
|
+
const newPriority = SCOPE_PRIORITY[skill.scope];
|
|
4248
|
+
if (newPriority < existingPriority) {
|
|
4249
|
+
byName.set(skill.name, skill);
|
|
4250
|
+
} else if (newPriority === existingPriority) {
|
|
4251
|
+
byName.set(skill.name, skill);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
return Array.from(byName.values()).sort(
|
|
4255
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
4256
|
+
);
|
|
4257
|
+
}
|
|
4258
|
+
async function dirExists(path2) {
|
|
4259
|
+
try {
|
|
4260
|
+
const info = await stat5(path2);
|
|
4261
|
+
return info.isDirectory();
|
|
4262
|
+
} catch {
|
|
4263
|
+
return false;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
async function findGitRoot2(startDir) {
|
|
4267
|
+
let dir = resolve4(startDir);
|
|
4268
|
+
const root = sep2 === "/" ? "/" : dir.slice(0, 3);
|
|
4269
|
+
while (dir !== root) {
|
|
4270
|
+
try {
|
|
4271
|
+
await access2(join5(dir, ".git"));
|
|
4272
|
+
return dir;
|
|
4273
|
+
} catch {
|
|
4274
|
+
const parent = dirname3(dir);
|
|
4275
|
+
if (parent === dir) break;
|
|
4276
|
+
dir = parent;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
return void 0;
|
|
4280
|
+
}
|
|
4281
|
+
function dirsBetween(from, to) {
|
|
4282
|
+
const fromResolved = resolve4(from);
|
|
4283
|
+
const toResolved = resolve4(to);
|
|
4284
|
+
if (!toResolved.startsWith(fromResolved)) return [fromResolved];
|
|
4285
|
+
const dirs = [];
|
|
4286
|
+
let current = toResolved;
|
|
4287
|
+
while (current.startsWith(fromResolved)) {
|
|
4288
|
+
dirs.push(current);
|
|
4289
|
+
const parent = dirname3(current);
|
|
4290
|
+
if (parent === current) break;
|
|
4291
|
+
current = parent;
|
|
4292
|
+
}
|
|
4293
|
+
dirs.reverse();
|
|
4294
|
+
return dirs;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// src/skill/registry.ts
|
|
4298
|
+
var SkillRegistry = class {
|
|
4299
|
+
/** All discovered skill metadata indexed by name */
|
|
4300
|
+
skills;
|
|
4301
|
+
/** Cached full content for skills that have been loaded */
|
|
4302
|
+
contentCache;
|
|
4303
|
+
/** Discovery metadata */
|
|
4304
|
+
discoveryResult;
|
|
4305
|
+
constructor(discoveryResult) {
|
|
4306
|
+
this.discoveryResult = discoveryResult;
|
|
4307
|
+
this.skills = /* @__PURE__ */ new Map();
|
|
4308
|
+
this.contentCache = /* @__PURE__ */ new Map();
|
|
4309
|
+
for (const skill of discoveryResult.skills) {
|
|
4310
|
+
this.skills.set(skill.name, skill);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
// ==========================================================================
|
|
4314
|
+
// Lookup
|
|
4315
|
+
// ==========================================================================
|
|
4316
|
+
/** Get a skill's metadata by name. Returns undefined if not found. */
|
|
4317
|
+
get(name) {
|
|
4318
|
+
return this.skills.get(name);
|
|
4319
|
+
}
|
|
4320
|
+
/** Check if a skill exists by name. */
|
|
4321
|
+
has(name) {
|
|
4322
|
+
return this.skills.has(name);
|
|
4323
|
+
}
|
|
4324
|
+
/** Get all skill metadata entries. */
|
|
4325
|
+
list() {
|
|
4326
|
+
return Array.from(this.skills.values());
|
|
4327
|
+
}
|
|
4328
|
+
/** Number of registered skills. */
|
|
4329
|
+
get size() {
|
|
4330
|
+
return this.skills.size;
|
|
4331
|
+
}
|
|
4332
|
+
/** Skill names as an array. */
|
|
4333
|
+
get names() {
|
|
4334
|
+
return Array.from(this.skills.keys());
|
|
4335
|
+
}
|
|
4336
|
+
// ==========================================================================
|
|
4337
|
+
// Content Loading (L2 + L3)
|
|
4338
|
+
// ==========================================================================
|
|
4339
|
+
/**
|
|
4340
|
+
* Load a skill's full content (L2: body + resource listing).
|
|
4341
|
+
*
|
|
4342
|
+
* Results are cached — subsequent calls return the cached content
|
|
4343
|
+
* without re-reading the filesystem.
|
|
4344
|
+
*
|
|
4345
|
+
* @param name Skill name
|
|
4346
|
+
* @returns Full skill content, or null if the skill doesn't exist
|
|
4347
|
+
*/
|
|
4348
|
+
async loadContent(name) {
|
|
4349
|
+
const cached = this.contentCache.get(name);
|
|
4350
|
+
if (cached) return cached;
|
|
4351
|
+
const metadata = this.skills.get(name);
|
|
4352
|
+
if (!metadata) return null;
|
|
4353
|
+
const content = await loadSkillContent(metadata);
|
|
4354
|
+
this.contentCache.set(name, content);
|
|
4355
|
+
return content;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Load a specific bundled resource from a skill (L3).
|
|
4359
|
+
*
|
|
4360
|
+
* The skill's content must be loaded first (via `loadContent`)
|
|
4361
|
+
* so the resource listing is available.
|
|
4362
|
+
*
|
|
4363
|
+
* @param skillName Skill name
|
|
4364
|
+
* @param relativePath Relative path to the resource within the skill dir
|
|
4365
|
+
* @returns Resource file content as UTF-8 string
|
|
4366
|
+
* @throws If the skill or resource doesn't exist
|
|
4367
|
+
*/
|
|
4368
|
+
async loadResource(skillName, relativePath) {
|
|
4369
|
+
const content = await this.loadContent(skillName);
|
|
4370
|
+
if (!content) {
|
|
4371
|
+
throw new Error(`Skill not found: "${skillName}"`);
|
|
4372
|
+
}
|
|
4373
|
+
const resource = content.resources.find(
|
|
4374
|
+
(r) => r.relativePath === relativePath
|
|
4375
|
+
);
|
|
4376
|
+
if (!resource) {
|
|
4377
|
+
const available = content.resources.map((r) => r.relativePath).join(", ");
|
|
4378
|
+
throw new Error(
|
|
4379
|
+
`Resource "${relativePath}" not found in skill "${skillName}". Available: ${available || "(none)"}`
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
4382
|
+
return loadResourceContent(resource);
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Get the list of resources for a loaded skill.
|
|
4386
|
+
*
|
|
4387
|
+
* @param name Skill name
|
|
4388
|
+
* @returns Resource list, or empty array if skill isn't loaded yet
|
|
4389
|
+
*/
|
|
4390
|
+
getResources(name) {
|
|
4391
|
+
return this.contentCache.get(name)?.resources ?? [];
|
|
4392
|
+
}
|
|
4393
|
+
// ==========================================================================
|
|
4394
|
+
// Prompt Summary (L1 — always in system prompt)
|
|
4395
|
+
// ==========================================================================
|
|
4396
|
+
/**
|
|
4397
|
+
* Format a summary of all available skills for injection into the system prompt.
|
|
4398
|
+
*
|
|
4399
|
+
* This is the L1 layer — the agent sees names and descriptions of all
|
|
4400
|
+
* available skills, enabling it to decide which to activate via tool calls.
|
|
4401
|
+
*
|
|
4402
|
+
* The output format uses XML tags (consistent with the instruction format
|
|
4403
|
+
* in `formatInstructions`) for clear delineation in the prompt.
|
|
4404
|
+
*
|
|
4405
|
+
* Returns empty string if no skills are available.
|
|
4406
|
+
*
|
|
4407
|
+
* @example Output:
|
|
4408
|
+
* ```xml
|
|
4409
|
+
* <available-skills>
|
|
4410
|
+
*
|
|
4411
|
+
* You have access to the following skills. To activate a skill and load its
|
|
4412
|
+
* full instructions, call the `skill` tool with the skill's name.
|
|
4413
|
+
*
|
|
4414
|
+
* <skill name="testing" scope="project">
|
|
4415
|
+
* Write comprehensive test suites with vitest, covering unit, integration,
|
|
4416
|
+
* and snapshot testing patterns.
|
|
4417
|
+
* </skill>
|
|
4418
|
+
*
|
|
4419
|
+
* <skill name="frontend-design" scope="user">
|
|
4420
|
+
* Create distinctive, production-grade frontend interfaces with high design quality.
|
|
4421
|
+
* </skill>
|
|
4422
|
+
*
|
|
4423
|
+
* </available-skills>
|
|
4424
|
+
* ```
|
|
4425
|
+
*/
|
|
4426
|
+
formatSummary() {
|
|
4427
|
+
if (this.skills.size === 0) return "";
|
|
4428
|
+
const lines = [];
|
|
4429
|
+
lines.push("<available-skills>");
|
|
4430
|
+
lines.push("");
|
|
4431
|
+
lines.push(
|
|
4432
|
+
"You have access to the following skills. To activate a skill and load its full instructions, call the `skill` tool with the skill's name. Only load a skill when the current task matches its description."
|
|
4433
|
+
);
|
|
4434
|
+
for (const skill of this.skills.values()) {
|
|
4435
|
+
lines.push("");
|
|
4436
|
+
lines.push(`<skill name="${skill.name}" scope="${skill.scope}">`);
|
|
4437
|
+
lines.push(skill.description);
|
|
4438
|
+
if (skill.dependencies && skill.dependencies.length > 0) {
|
|
4439
|
+
lines.push(`Depends on: ${skill.dependencies.join(", ")}`);
|
|
4440
|
+
}
|
|
4441
|
+
lines.push("</skill>");
|
|
4442
|
+
}
|
|
4443
|
+
lines.push("");
|
|
4444
|
+
lines.push("</available-skills>");
|
|
4445
|
+
return lines.join("\n");
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Format the full content of a loaded skill for a tool response.
|
|
4449
|
+
*
|
|
4450
|
+
* Wraps the skill body in XML with metadata, and lists bundled resources
|
|
4451
|
+
* so the agent knows what's available at L3.
|
|
4452
|
+
*
|
|
4453
|
+
* @param content Previously loaded skill content
|
|
4454
|
+
* @returns Formatted string for tool response
|
|
4455
|
+
*/
|
|
4456
|
+
formatContent(content) {
|
|
4457
|
+
const lines = [];
|
|
4458
|
+
lines.push(`<skill-content name="${content.name}">`);
|
|
4459
|
+
lines.push("");
|
|
4460
|
+
lines.push(content.body);
|
|
4461
|
+
if (content.resources.length > 0) {
|
|
4462
|
+
lines.push("");
|
|
4463
|
+
lines.push("<bundled-resources>");
|
|
4464
|
+
lines.push(
|
|
4465
|
+
"This skill includes the following bundled files. To read one, call the `skill_resource` tool with the skill name and relative path."
|
|
4466
|
+
);
|
|
4467
|
+
lines.push("");
|
|
4468
|
+
const byType = /* @__PURE__ */ new Map();
|
|
4469
|
+
for (const r of content.resources) {
|
|
4470
|
+
const list = byType.get(r.type) ?? [];
|
|
4471
|
+
list.push(r);
|
|
4472
|
+
byType.set(r.type, list);
|
|
4473
|
+
}
|
|
4474
|
+
for (const [type, resources] of byType) {
|
|
4475
|
+
lines.push(` ${type}s:`);
|
|
4476
|
+
for (const r of resources) {
|
|
4477
|
+
lines.push(` - ${r.relativePath}`);
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
lines.push("</bundled-resources>");
|
|
4481
|
+
}
|
|
4482
|
+
if (content.dependencies && content.dependencies.length > 0) {
|
|
4483
|
+
lines.push("");
|
|
4484
|
+
lines.push(
|
|
4485
|
+
`<dependencies>This skill works best when combined with: ${content.dependencies.join(", ")}</dependencies>`
|
|
4486
|
+
);
|
|
4487
|
+
}
|
|
4488
|
+
lines.push("");
|
|
4489
|
+
lines.push("</skill-content>");
|
|
4490
|
+
return lines.join("\n");
|
|
4491
|
+
}
|
|
4492
|
+
// ==========================================================================
|
|
4493
|
+
// Cache Management
|
|
4494
|
+
// ==========================================================================
|
|
4495
|
+
/** Clear the content cache, forcing reloads on next access. */
|
|
4496
|
+
clearContentCache() {
|
|
4497
|
+
this.contentCache.clear();
|
|
4498
|
+
}
|
|
4499
|
+
/** Check if a skill's content has been loaded and cached. */
|
|
4500
|
+
isContentLoaded(name) {
|
|
4501
|
+
return this.contentCache.has(name);
|
|
4502
|
+
}
|
|
4503
|
+
};
|
|
4504
|
+
async function createSkillRegistry(cwd, config) {
|
|
4505
|
+
const result = await discoverSkills(cwd, config);
|
|
4506
|
+
return new SkillRegistry(result);
|
|
4507
|
+
}
|
|
4508
|
+
function emptySkillRegistry() {
|
|
4509
|
+
return new SkillRegistry({
|
|
4510
|
+
skills: [],
|
|
4511
|
+
errors: [],
|
|
4512
|
+
dirsScanned: 0,
|
|
4513
|
+
durationMs: 0
|
|
4514
|
+
});
|
|
4515
|
+
}
|
|
4516
|
+
|
|
3872
4517
|
// src/prompt/builder.ts
|
|
3873
4518
|
var PRIORITY_BASE = 10;
|
|
3874
4519
|
var PRIORITY_ENVIRONMENT = 20;
|
|
@@ -3884,6 +4529,8 @@ var PromptBuilder = class {
|
|
|
3884
4529
|
envCache;
|
|
3885
4530
|
/** Cached instruction files */
|
|
3886
4531
|
instructionCache;
|
|
4532
|
+
/** Cached skill registry */
|
|
4533
|
+
skillRegistryCache;
|
|
3887
4534
|
constructor(config) {
|
|
3888
4535
|
this.config = {
|
|
3889
4536
|
includeEnvironment: true,
|
|
@@ -3910,9 +4557,10 @@ var PromptBuilder = class {
|
|
|
3910
4557
|
* with the configured separator.
|
|
3911
4558
|
*
|
|
3912
4559
|
* @param context - Build context with cwd, model, and optional override
|
|
4560
|
+
* @param middleware - Optional middleware runner to collect dynamic sections from
|
|
3913
4561
|
* @returns The composed system prompt string
|
|
3914
4562
|
*/
|
|
3915
|
-
async build(context) {
|
|
4563
|
+
async build(context, middleware) {
|
|
3916
4564
|
const sections = [];
|
|
3917
4565
|
const family = this.config.modelFamily ?? detectModelFamily(context.model);
|
|
3918
4566
|
const templateContent = this.config.baseTemplate ?? getTemplate(family);
|
|
@@ -3942,9 +4590,25 @@ var PromptBuilder = class {
|
|
|
3942
4590
|
});
|
|
3943
4591
|
}
|
|
3944
4592
|
}
|
|
4593
|
+
if (this.config.skills) {
|
|
4594
|
+
const registry = await this.getSkillRegistry(context.cwd);
|
|
4595
|
+
const summary = registry.formatSummary();
|
|
4596
|
+
if (summary.length > 0) {
|
|
4597
|
+
sections.push({
|
|
4598
|
+
id: "skills",
|
|
4599
|
+
label: "Available Skills",
|
|
4600
|
+
content: summary,
|
|
4601
|
+
priority: PRIORITY_SKILLS
|
|
4602
|
+
});
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
3945
4605
|
for (const section of this.customSections.values()) {
|
|
3946
4606
|
sections.push(section);
|
|
3947
4607
|
}
|
|
4608
|
+
if (middleware?.hasMiddleware) {
|
|
4609
|
+
const mwSections = middleware.collectPromptSections(context);
|
|
4610
|
+
sections.push(...mwSections);
|
|
4611
|
+
}
|
|
3948
4612
|
if (context.override) {
|
|
3949
4613
|
sections.push({
|
|
3950
4614
|
id: "override",
|
|
@@ -4031,6 +4695,29 @@ var PromptBuilder = class {
|
|
|
4031
4695
|
clearCache() {
|
|
4032
4696
|
this.envCache = void 0;
|
|
4033
4697
|
this.instructionCache = void 0;
|
|
4698
|
+
this.skillRegistryCache = void 0;
|
|
4699
|
+
}
|
|
4700
|
+
/**
|
|
4701
|
+
* Get the skill registry for the current working directory.
|
|
4702
|
+
*
|
|
4703
|
+
* Runs skill discovery on first call and caches the result.
|
|
4704
|
+
* Consumers can use this to get the registry for creating skill tools.
|
|
4705
|
+
*
|
|
4706
|
+
* @param cwd Working directory for skill discovery
|
|
4707
|
+
* @returns The skill registry (may be empty if no skills config)
|
|
4708
|
+
*/
|
|
4709
|
+
async getSkillRegistry(cwd) {
|
|
4710
|
+
if (this.skillRegistryCache && this.skillRegistryCache.cwd === cwd) {
|
|
4711
|
+
return this.skillRegistryCache.registry;
|
|
4712
|
+
}
|
|
4713
|
+
if (!this.config.skills) {
|
|
4714
|
+
const registry2 = emptySkillRegistry();
|
|
4715
|
+
this.skillRegistryCache = { cwd, registry: registry2 };
|
|
4716
|
+
return registry2;
|
|
4717
|
+
}
|
|
4718
|
+
const registry = await createSkillRegistry(cwd, this.config.skills);
|
|
4719
|
+
this.skillRegistryCache = { cwd, registry };
|
|
4720
|
+
return registry;
|
|
4034
4721
|
}
|
|
4035
4722
|
// ============================================================================
|
|
4036
4723
|
// Introspection
|
|
@@ -4324,7 +5011,7 @@ function localHost(defaultCwd) {
|
|
|
4324
5011
|
PAGER: "cat",
|
|
4325
5012
|
GIT_PAGER: "cat"
|
|
4326
5013
|
};
|
|
4327
|
-
return new Promise((
|
|
5014
|
+
return new Promise((resolve6, reject) => {
|
|
4328
5015
|
let stdout = "";
|
|
4329
5016
|
let stderr = "";
|
|
4330
5017
|
let timedOut = false;
|
|
@@ -4354,12 +5041,12 @@ function localHost(defaultCwd) {
|
|
|
4354
5041
|
if (child.pid) killProcessTree(child.pid);
|
|
4355
5042
|
};
|
|
4356
5043
|
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
4357
|
-
child.on("close", (
|
|
5044
|
+
child.on("close", (code2) => {
|
|
4358
5045
|
if (settled) return;
|
|
4359
5046
|
settled = true;
|
|
4360
5047
|
if (timer) clearTimeout(timer);
|
|
4361
5048
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
4362
|
-
|
|
5049
|
+
resolve6({ stdout, stderr, exitCode: code2, timedOut });
|
|
4363
5050
|
});
|
|
4364
5051
|
child.on("error", (err) => {
|
|
4365
5052
|
if (settled) return;
|
|
@@ -4390,7 +5077,7 @@ async function containerExec(container, docker, command, options = {}) {
|
|
|
4390
5077
|
const stream = await exec.start({});
|
|
4391
5078
|
let timedOut = false;
|
|
4392
5079
|
let aborted = false;
|
|
4393
|
-
return new Promise((
|
|
5080
|
+
return new Promise((resolve6, reject) => {
|
|
4394
5081
|
const stdoutChunks = [];
|
|
4395
5082
|
const stderrChunks = [];
|
|
4396
5083
|
docker.modem.demuxStream(
|
|
@@ -4433,7 +5120,7 @@ async function containerExec(container, docker, command, options = {}) {
|
|
|
4433
5120
|
options.signal?.removeEventListener("abort", onAbort);
|
|
4434
5121
|
try {
|
|
4435
5122
|
const info = await exec.inspect();
|
|
4436
|
-
|
|
5123
|
+
resolve6({
|
|
4437
5124
|
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
4438
5125
|
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
4439
5126
|
exitCode: timedOut || aborted ? null : info.ExitCode ?? 0,
|
|
@@ -4447,7 +5134,7 @@ async function containerExec(container, docker, command, options = {}) {
|
|
|
4447
5134
|
if (timer) clearTimeout(timer);
|
|
4448
5135
|
options.signal?.removeEventListener("abort", onAbort);
|
|
4449
5136
|
if (timedOut || aborted) {
|
|
4450
|
-
|
|
5137
|
+
resolve6({
|
|
4451
5138
|
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
4452
5139
|
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
4453
5140
|
exitCode: null,
|
|
@@ -4595,11 +5282,180 @@ function resolvePath(p, defaultWorkdir) {
|
|
|
4595
5282
|
return base + p;
|
|
4596
5283
|
}
|
|
4597
5284
|
|
|
5285
|
+
// src/middleware/runner.ts
|
|
5286
|
+
var MiddlewareRunner = class {
|
|
5287
|
+
stack;
|
|
5288
|
+
constructor(middleware = []) {
|
|
5289
|
+
this.stack = Object.freeze([...middleware]);
|
|
5290
|
+
}
|
|
5291
|
+
/** Number of registered middleware */
|
|
5292
|
+
get count() {
|
|
5293
|
+
return this.stack.length;
|
|
5294
|
+
}
|
|
5295
|
+
/** Whether any middleware is registered */
|
|
5296
|
+
get hasMiddleware() {
|
|
5297
|
+
return this.stack.length > 0;
|
|
5298
|
+
}
|
|
5299
|
+
/** Get the middleware list (for fork inheritance) */
|
|
5300
|
+
getMiddleware() {
|
|
5301
|
+
return this.stack;
|
|
5302
|
+
}
|
|
5303
|
+
// --------------------------------------------------------------------------
|
|
5304
|
+
// beforeToolCall — array order, first "deny" wins
|
|
5305
|
+
// --------------------------------------------------------------------------
|
|
5306
|
+
/**
|
|
5307
|
+
* Run all `beforeToolCall` hooks in order.
|
|
5308
|
+
*
|
|
5309
|
+
* Returns `{ action: "allow" }` if all middleware allow (or have no hook).
|
|
5310
|
+
* Returns `{ action: "deny", reason }` on first denial — remaining
|
|
5311
|
+
* middleware are skipped.
|
|
5312
|
+
*/
|
|
5313
|
+
async runBeforeToolCall(tool2, args, ctx) {
|
|
5314
|
+
for (const mw of this.stack) {
|
|
5315
|
+
if (!mw.beforeToolCall) continue;
|
|
5316
|
+
try {
|
|
5317
|
+
const decision = await mw.beforeToolCall(tool2, args, ctx);
|
|
5318
|
+
if (decision.action === "deny") {
|
|
5319
|
+
return decision;
|
|
5320
|
+
}
|
|
5321
|
+
} catch (err) {
|
|
5322
|
+
return {
|
|
5323
|
+
action: "deny",
|
|
5324
|
+
reason: `Middleware "${mw.name}" error: ${err instanceof Error ? err.message : String(err)}`
|
|
5325
|
+
};
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
return { action: "allow" };
|
|
5329
|
+
}
|
|
5330
|
+
// --------------------------------------------------------------------------
|
|
5331
|
+
// afterToolCall — reverse order (innermost first)
|
|
5332
|
+
// --------------------------------------------------------------------------
|
|
5333
|
+
/**
|
|
5334
|
+
* Run all `afterToolCall` hooks in reverse order.
|
|
5335
|
+
*
|
|
5336
|
+
* Each hook receives the result from the previous hook (or the
|
|
5337
|
+
* original tool result for the first hook). Errors are caught
|
|
5338
|
+
* and logged — the original result passes through.
|
|
5339
|
+
*/
|
|
5340
|
+
async runAfterToolCall(tool2, args, result, ctx) {
|
|
5341
|
+
let current = result;
|
|
5342
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
5343
|
+
const mw = this.stack[i];
|
|
5344
|
+
if (!mw.afterToolCall) continue;
|
|
5345
|
+
try {
|
|
5346
|
+
current = await mw.afterToolCall(tool2, args, current, ctx);
|
|
5347
|
+
} catch (err) {
|
|
5348
|
+
console.warn(
|
|
5349
|
+
`[middleware] "${mw.name}" afterToolCall error:`,
|
|
5350
|
+
err instanceof Error ? err.message : String(err)
|
|
5351
|
+
);
|
|
5352
|
+
}
|
|
5353
|
+
}
|
|
5354
|
+
return current;
|
|
5355
|
+
}
|
|
5356
|
+
// --------------------------------------------------------------------------
|
|
5357
|
+
// promptSections — all run, results merged
|
|
5358
|
+
// --------------------------------------------------------------------------
|
|
5359
|
+
/**
|
|
5360
|
+
* Collect prompt sections from all middleware.
|
|
5361
|
+
*
|
|
5362
|
+
* Returns a flat array of sections. Each middleware can return a single
|
|
5363
|
+
* section, an array of sections, or undefined/empty.
|
|
5364
|
+
*/
|
|
5365
|
+
collectPromptSections(ctx) {
|
|
5366
|
+
const sections = [];
|
|
5367
|
+
for (const mw of this.stack) {
|
|
5368
|
+
if (!mw.promptSections) continue;
|
|
5369
|
+
try {
|
|
5370
|
+
const result = mw.promptSections(ctx);
|
|
5371
|
+
if (!result) continue;
|
|
5372
|
+
if (Array.isArray(result)) {
|
|
5373
|
+
sections.push(...result);
|
|
5374
|
+
} else {
|
|
5375
|
+
sections.push(result);
|
|
5376
|
+
}
|
|
5377
|
+
} catch (err) {
|
|
5378
|
+
console.warn(
|
|
5379
|
+
`[middleware] "${mw.name}" promptSections error:`,
|
|
5380
|
+
err instanceof Error ? err.message : String(err)
|
|
5381
|
+
);
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
return sections;
|
|
5385
|
+
}
|
|
5386
|
+
// --------------------------------------------------------------------------
|
|
5387
|
+
// onEvent — fire-and-forget, all run
|
|
5388
|
+
// --------------------------------------------------------------------------
|
|
5389
|
+
/**
|
|
5390
|
+
* Broadcast an event to all middleware observers.
|
|
5391
|
+
*
|
|
5392
|
+
* Non-blocking — errors are caught and logged. This never
|
|
5393
|
+
* slows down the streaming pipeline.
|
|
5394
|
+
*/
|
|
5395
|
+
emitEvent(event) {
|
|
5396
|
+
for (const mw of this.stack) {
|
|
5397
|
+
if (!mw.onEvent) continue;
|
|
5398
|
+
try {
|
|
5399
|
+
mw.onEvent(event);
|
|
5400
|
+
} catch {
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
// --------------------------------------------------------------------------
|
|
5405
|
+
// onChatStart — array order, sequential
|
|
5406
|
+
// --------------------------------------------------------------------------
|
|
5407
|
+
/**
|
|
5408
|
+
* Run all `onChatStart` hooks in order.
|
|
5409
|
+
*
|
|
5410
|
+
* Errors are caught and logged — a broken logger should not
|
|
5411
|
+
* prevent the chat from starting.
|
|
5412
|
+
*/
|
|
5413
|
+
async runChatStart(sessionId, message) {
|
|
5414
|
+
for (const mw of this.stack) {
|
|
5415
|
+
if (!mw.onChatStart) continue;
|
|
5416
|
+
try {
|
|
5417
|
+
await mw.onChatStart(sessionId, message);
|
|
5418
|
+
} catch (err) {
|
|
5419
|
+
console.warn(
|
|
5420
|
+
`[middleware] "${mw.name}" onChatStart error:`,
|
|
5421
|
+
err instanceof Error ? err.message : String(err)
|
|
5422
|
+
);
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
// --------------------------------------------------------------------------
|
|
5427
|
+
// onChatEnd — array order, sequential
|
|
5428
|
+
// --------------------------------------------------------------------------
|
|
5429
|
+
/**
|
|
5430
|
+
* Run all `onChatEnd` hooks in order.
|
|
5431
|
+
*
|
|
5432
|
+
* Always called, even when the stream errored. Errors in handlers
|
|
5433
|
+
* are caught and logged.
|
|
5434
|
+
*/
|
|
5435
|
+
async runChatEnd(sessionId, result) {
|
|
5436
|
+
for (const mw of this.stack) {
|
|
5437
|
+
if (!mw.onChatEnd) continue;
|
|
5438
|
+
try {
|
|
5439
|
+
await mw.onChatEnd(sessionId, result);
|
|
5440
|
+
} catch (err) {
|
|
5441
|
+
console.warn(
|
|
5442
|
+
`[middleware] "${mw.name}" onChatEnd error:`,
|
|
5443
|
+
err instanceof Error ? err.message : String(err)
|
|
5444
|
+
);
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
5449
|
+
|
|
4598
5450
|
// src/agent.ts
|
|
4599
5451
|
var DEFAULT_SYSTEM_PROMPT = `You are a capable AI assistant with access to tools.
|
|
5452
|
+
|
|
4600
5453
|
Think step by step about what you need to do.
|
|
4601
5454
|
Use the available tools to accomplish tasks.
|
|
4602
|
-
Verify your results after each action
|
|
5455
|
+
Verify your results after each action.
|
|
5456
|
+
|
|
5457
|
+
If a tool fails, try an alternative approach \u2014 do not give up after a single error.
|
|
5458
|
+
Keep working until the task is fully resolved or you have exhausted all options.`;
|
|
4603
5459
|
var DEFAULT_MAX_STEPS = 50;
|
|
4604
5460
|
var DEFAULT_MAX_TOKENS = 32e3;
|
|
4605
5461
|
var DEFAULT_CUSTOM_STREAM_MODELS = [
|
|
@@ -4676,6 +5532,8 @@ var Agent = class _Agent {
|
|
|
4676
5532
|
interventionCtrl;
|
|
4677
5533
|
/** Execution environment for tool operations */
|
|
4678
5534
|
host;
|
|
5535
|
+
/** Middleware runner for lifecycle hooks */
|
|
5536
|
+
middlewareRunner;
|
|
4679
5537
|
constructor(config) {
|
|
4680
5538
|
if (config.prompt !== void 0) {
|
|
4681
5539
|
this.promptBuilder = createPromptBuilder(config.prompt);
|
|
@@ -4741,6 +5599,7 @@ var Agent = class _Agent {
|
|
|
4741
5599
|
this.mcpManager = config.mcp;
|
|
4742
5600
|
this.interventionCtrl = new InterventionController();
|
|
4743
5601
|
this.host = config.host ?? localHost(this.config.cwd);
|
|
5602
|
+
this.middlewareRunner = new MiddlewareRunner(config.middleware);
|
|
4744
5603
|
if (config.prompt !== void 0) {
|
|
4745
5604
|
this.config.prompt = config.prompt;
|
|
4746
5605
|
}
|
|
@@ -4932,7 +5791,12 @@ var Agent = class _Agent {
|
|
|
4932
5791
|
const mcpTools = await this.ensureMCPConnected();
|
|
4933
5792
|
this.state.isStreaming = true;
|
|
4934
5793
|
const prevOnApplied = this.interventionCtrl.onApplied;
|
|
5794
|
+
let chatUsage;
|
|
5795
|
+
let chatError;
|
|
4935
5796
|
try {
|
|
5797
|
+
if (this.middlewareRunner.hasMiddleware) {
|
|
5798
|
+
await this.middlewareRunner.runChatStart(sessionId, message);
|
|
5799
|
+
}
|
|
4936
5800
|
let systemPrompts;
|
|
4937
5801
|
if (this.promptBuilder) {
|
|
4938
5802
|
const composedPrompt = await this.promptBuilder.build({
|
|
@@ -4941,7 +5805,7 @@ var Agent = class _Agent {
|
|
|
4941
5805
|
toolNames: Array.from(this.tools.keys()),
|
|
4942
5806
|
override: options?.system,
|
|
4943
5807
|
sessionId
|
|
4944
|
-
});
|
|
5808
|
+
}, this.middlewareRunner);
|
|
4945
5809
|
systemPrompts = [composedPrompt];
|
|
4946
5810
|
} else {
|
|
4947
5811
|
systemPrompts = [this.config.systemPrompt];
|
|
@@ -4972,7 +5836,9 @@ var Agent = class _Agent {
|
|
|
4972
5836
|
turnTracker: this.turnTracker,
|
|
4973
5837
|
// Pass intervention controller for mid-turn message injection.
|
|
4974
5838
|
// Only effective when using the standard AI SDK path (not custom providers).
|
|
4975
|
-
intervention: this.interventionCtrl
|
|
5839
|
+
intervention: this.interventionCtrl,
|
|
5840
|
+
// Pass middleware runner for tool lifecycle hooks
|
|
5841
|
+
middleware: this.middlewareRunner
|
|
4976
5842
|
});
|
|
4977
5843
|
const eventQueue = [];
|
|
4978
5844
|
let resolveNext = null;
|
|
@@ -5005,6 +5871,7 @@ var Agent = class _Agent {
|
|
|
5005
5871
|
onContextOverflow: async (_tokens, _limit) => {
|
|
5006
5872
|
},
|
|
5007
5873
|
onEvent: async (event) => {
|
|
5874
|
+
this.middlewareRunner.emitEvent(event);
|
|
5008
5875
|
eventQueue.push(event);
|
|
5009
5876
|
if (resolveNext) {
|
|
5010
5877
|
resolveNext();
|
|
@@ -5097,8 +5964,8 @@ var Agent = class _Agent {
|
|
|
5097
5964
|
}
|
|
5098
5965
|
}
|
|
5099
5966
|
if (!streamDone) {
|
|
5100
|
-
await new Promise((
|
|
5101
|
-
resolveNext =
|
|
5967
|
+
await new Promise((resolve6) => {
|
|
5968
|
+
resolveNext = resolve6;
|
|
5102
5969
|
});
|
|
5103
5970
|
}
|
|
5104
5971
|
}
|
|
@@ -5146,10 +6013,20 @@ var Agent = class _Agent {
|
|
|
5146
6013
|
deletions: turnSummary.deletions
|
|
5147
6014
|
};
|
|
5148
6015
|
}
|
|
6016
|
+
chatUsage = result?.usage;
|
|
5149
6017
|
yield { type: "complete", usage: result?.usage };
|
|
6018
|
+
} catch (err) {
|
|
6019
|
+
chatError = err instanceof Error ? err : new Error(String(err));
|
|
6020
|
+
throw err;
|
|
5150
6021
|
} finally {
|
|
5151
6022
|
this.state.isStreaming = false;
|
|
5152
6023
|
this.interventionCtrl.onApplied = prevOnApplied;
|
|
6024
|
+
if (this.middlewareRunner.hasMiddleware) {
|
|
6025
|
+
await this.middlewareRunner.runChatEnd(sessionId, {
|
|
6026
|
+
usage: chatUsage,
|
|
6027
|
+
error: chatError
|
|
6028
|
+
});
|
|
6029
|
+
}
|
|
5153
6030
|
}
|
|
5154
6031
|
}
|
|
5155
6032
|
/**
|
|
@@ -5598,7 +6475,9 @@ var Agent = class _Agent {
|
|
|
5598
6475
|
compaction: this.config.compaction,
|
|
5599
6476
|
contextWindow: this.config.contextWindow,
|
|
5600
6477
|
// Share MCP manager (connections are shared)
|
|
5601
|
-
mcp: this.mcpManager
|
|
6478
|
+
mcp: this.mcpManager,
|
|
6479
|
+
// Middleware: explicit override > additional appended > inherit parent's
|
|
6480
|
+
middleware: effectiveOptions.middleware ?? (effectiveOptions.additionalMiddleware ? [...this.middlewareRunner.getMiddleware(), ...effectiveOptions.additionalMiddleware] : [...this.middlewareRunner.getMiddleware()])
|
|
5602
6481
|
});
|
|
5603
6482
|
}
|
|
5604
6483
|
/**
|
|
@@ -5710,12 +6589,22 @@ var Agent = class _Agent {
|
|
|
5710
6589
|
// src/safety/approval.ts
|
|
5711
6590
|
var DEFAULT_TOOL_RISKS = {
|
|
5712
6591
|
// Safe - read-only operations
|
|
6592
|
+
read: "safe",
|
|
5713
6593
|
read_file: "safe",
|
|
5714
6594
|
grep: "safe",
|
|
5715
6595
|
glob: "safe",
|
|
5716
6596
|
list_dir: "safe",
|
|
6597
|
+
// Safe - sub-agent delegation (sub-agent's own tools go through approval)
|
|
6598
|
+
invoke_agent: "safe",
|
|
6599
|
+
wait_agent: "safe",
|
|
6600
|
+
close_agent: "safe",
|
|
6601
|
+
// Safe - skill loading (read-only knowledge retrieval)
|
|
6602
|
+
skill: "safe",
|
|
6603
|
+
skill_resource: "safe",
|
|
5717
6604
|
// Moderate - file modifications
|
|
6605
|
+
write: "moderate",
|
|
5718
6606
|
write_file: "moderate",
|
|
6607
|
+
edit: "moderate",
|
|
5719
6608
|
edit_file: "moderate",
|
|
5720
6609
|
create_file: "moderate",
|
|
5721
6610
|
// Dangerous - system commands, deletions
|
|
@@ -5736,9 +6625,10 @@ function matchPattern(pattern, value) {
|
|
|
5736
6625
|
function extractPatterns(tool2, args) {
|
|
5737
6626
|
if (!args || typeof args !== "object") return [tool2];
|
|
5738
6627
|
const a = args;
|
|
5739
|
-
if ("path" in a && typeof a.path === "string") {
|
|
5740
|
-
const
|
|
5741
|
-
|
|
6628
|
+
if ("path" in a && typeof a.path === "string" || "filePath" in a && typeof a.filePath === "string") {
|
|
6629
|
+
const p = a.path ?? a.filePath;
|
|
6630
|
+
const dir = p.substring(0, p.lastIndexOf("/") + 1);
|
|
6631
|
+
return [dir ? `${dir}*` : p];
|
|
5742
6632
|
}
|
|
5743
6633
|
if ("command" in a && typeof a.command === "string") {
|
|
5744
6634
|
const cmd = a.command.split(/\s+/)[0];
|
|
@@ -5753,13 +6643,16 @@ function describeOperation(tool2, args) {
|
|
|
5753
6643
|
if (!args || typeof args !== "object") return `Execute ${tool2}`;
|
|
5754
6644
|
const a = args;
|
|
5755
6645
|
switch (tool2) {
|
|
6646
|
+
case "read":
|
|
5756
6647
|
case "read_file":
|
|
5757
|
-
return `Read file: ${a.path}`;
|
|
6648
|
+
return `Read file: ${a.path ?? a.filePath}`;
|
|
6649
|
+
case "write":
|
|
5758
6650
|
case "write_file":
|
|
5759
6651
|
case "create_file":
|
|
5760
|
-
return `Write file: ${a.path}`;
|
|
6652
|
+
return `Write file: ${a.path ?? a.filePath}`;
|
|
6653
|
+
case "edit":
|
|
5761
6654
|
case "edit_file":
|
|
5762
|
-
return `Edit file: ${a.path}`;
|
|
6655
|
+
return `Edit file: ${a.path ?? a.filePath}`;
|
|
5763
6656
|
case "delete_file":
|
|
5764
6657
|
case "remove":
|
|
5765
6658
|
return `Delete: ${a.path}`;
|
|
@@ -5843,9 +6736,9 @@ function createApprovalHandler(config = {}) {
|
|
|
5843
6736
|
timestamp: Date.now()
|
|
5844
6737
|
};
|
|
5845
6738
|
const action = await Promise.race([
|
|
5846
|
-
new Promise((
|
|
5847
|
-
pending.set(id, { resolve:
|
|
5848
|
-
onRequest(request2).then(
|
|
6739
|
+
new Promise((resolve6, reject) => {
|
|
6740
|
+
pending.set(id, { resolve: resolve6, reject });
|
|
6741
|
+
onRequest(request2).then(resolve6).catch(reject).finally(() => pending.delete(id));
|
|
5849
6742
|
}),
|
|
5850
6743
|
new Promise((_, reject) => {
|
|
5851
6744
|
setTimeout(() => {
|
|
@@ -5892,12 +6785,12 @@ function createApprovalHandler(config = {}) {
|
|
|
5892
6785
|
|
|
5893
6786
|
// src/tracking/checkpoint.ts
|
|
5894
6787
|
import { spawn as spawn3 } from "child_process";
|
|
5895
|
-
import { mkdir as mkdir3, unlink as unlink3, readFile as
|
|
5896
|
-
import { join as
|
|
6788
|
+
import { mkdir as mkdir3, unlink as unlink3, readFile as readFile5, writeFile as writeFile3, access as access3, rm } from "fs/promises";
|
|
6789
|
+
import { join as join6, relative as relative3, resolve as resolve5, dirname as dirname4 } from "path";
|
|
5897
6790
|
import { createHash as createHash2 } from "crypto";
|
|
5898
|
-
import { homedir as
|
|
6791
|
+
import { homedir as homedir5 } from "os";
|
|
5899
6792
|
async function git(gitDir, workTree, args, _options = {}) {
|
|
5900
|
-
return new Promise((
|
|
6793
|
+
return new Promise((resolve6) => {
|
|
5901
6794
|
const fullArgs = ["--git-dir", gitDir, "--work-tree", workTree, ...args];
|
|
5902
6795
|
const proc = spawn3("git", fullArgs, {
|
|
5903
6796
|
cwd: workTree,
|
|
@@ -5916,12 +6809,12 @@ async function git(gitDir, workTree, args, _options = {}) {
|
|
|
5916
6809
|
proc.stderr.on("data", (data) => {
|
|
5917
6810
|
stderr += data.toString();
|
|
5918
6811
|
});
|
|
5919
|
-
proc.on("close", (
|
|
5920
|
-
const exitCode =
|
|
5921
|
-
|
|
6812
|
+
proc.on("close", (code2) => {
|
|
6813
|
+
const exitCode = code2 ?? 0;
|
|
6814
|
+
resolve6({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode });
|
|
5922
6815
|
});
|
|
5923
6816
|
proc.on("error", (err) => {
|
|
5924
|
-
|
|
6817
|
+
resolve6({ stdout: "", stderr: err.message, exitCode: 1 });
|
|
5925
6818
|
});
|
|
5926
6819
|
});
|
|
5927
6820
|
}
|
|
@@ -5930,11 +6823,11 @@ function projectId(workDir) {
|
|
|
5930
6823
|
}
|
|
5931
6824
|
function defaultStorageDir(workDir) {
|
|
5932
6825
|
const id = projectId(workDir);
|
|
5933
|
-
return
|
|
6826
|
+
return join6(homedir5(), ".cuylabs", "checkpoints", id);
|
|
5934
6827
|
}
|
|
5935
6828
|
async function readCheckpointLog(logPath) {
|
|
5936
6829
|
try {
|
|
5937
|
-
const content = await
|
|
6830
|
+
const content = await readFile5(logPath, "utf-8");
|
|
5938
6831
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5939
6832
|
return lines.map((line) => {
|
|
5940
6833
|
const entry = JSON.parse(line);
|
|
@@ -5952,9 +6845,9 @@ async function appendCheckpointLog(logPath, checkpoint) {
|
|
|
5952
6845
|
...checkpoint,
|
|
5953
6846
|
createdAt: checkpoint.createdAt.toISOString()
|
|
5954
6847
|
}) + "\n";
|
|
5955
|
-
await mkdir3(
|
|
6848
|
+
await mkdir3(dirname4(logPath), { recursive: true });
|
|
5956
6849
|
try {
|
|
5957
|
-
const existing = await
|
|
6850
|
+
const existing = await readFile5(logPath, "utf-8");
|
|
5958
6851
|
await writeFile3(logPath, existing + line);
|
|
5959
6852
|
} catch {
|
|
5960
6853
|
await writeFile3(logPath, line);
|
|
@@ -5962,7 +6855,7 @@ async function appendCheckpointLog(logPath, checkpoint) {
|
|
|
5962
6855
|
}
|
|
5963
6856
|
async function removeFromLog(logPath, checkpointId) {
|
|
5964
6857
|
try {
|
|
5965
|
-
const content = await
|
|
6858
|
+
const content = await readFile5(logPath, "utf-8");
|
|
5966
6859
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
5967
6860
|
const filtered = lines.filter((line) => {
|
|
5968
6861
|
const entry = JSON.parse(line);
|
|
@@ -5973,10 +6866,10 @@ async function removeFromLog(logPath, checkpointId) {
|
|
|
5973
6866
|
}
|
|
5974
6867
|
}
|
|
5975
6868
|
async function createCheckpointManager(config) {
|
|
5976
|
-
const workDir =
|
|
6869
|
+
const workDir = resolve5(config.workDir);
|
|
5977
6870
|
const storageDir = config.storageDir ?? defaultStorageDir(workDir);
|
|
5978
|
-
const gitDir =
|
|
5979
|
-
const logPath =
|
|
6871
|
+
const gitDir = join6(storageDir, "git");
|
|
6872
|
+
const logPath = join6(storageDir, "checkpoints.jsonl");
|
|
5980
6873
|
const state = {
|
|
5981
6874
|
config: {
|
|
5982
6875
|
workDir,
|
|
@@ -5992,23 +6885,23 @@ async function createCheckpointManager(config) {
|
|
|
5992
6885
|
async function initialize() {
|
|
5993
6886
|
if (state.initialized) return;
|
|
5994
6887
|
await mkdir3(gitDir, { recursive: true });
|
|
5995
|
-
const headPath =
|
|
6888
|
+
const headPath = join6(gitDir, "HEAD");
|
|
5996
6889
|
let needsInit = false;
|
|
5997
6890
|
try {
|
|
5998
|
-
await
|
|
6891
|
+
await access3(headPath);
|
|
5999
6892
|
} catch {
|
|
6000
6893
|
needsInit = true;
|
|
6001
6894
|
}
|
|
6002
6895
|
if (needsInit) {
|
|
6003
|
-
const initResult = await new Promise((
|
|
6896
|
+
const initResult = await new Promise((resolve6) => {
|
|
6004
6897
|
const proc = spawn3("git", ["init", "--bare"], {
|
|
6005
6898
|
cwd: gitDir,
|
|
6006
6899
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6007
6900
|
});
|
|
6008
6901
|
let stderr = "";
|
|
6009
6902
|
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
6010
|
-
proc.on("close", (
|
|
6011
|
-
proc.on("error", (err) =>
|
|
6903
|
+
proc.on("close", (code2) => resolve6({ exitCode: code2 ?? 0, stderr }));
|
|
6904
|
+
proc.on("error", (err) => resolve6({ exitCode: 1, stderr: err.message }));
|
|
6012
6905
|
});
|
|
6013
6906
|
if (initResult.exitCode !== 0) {
|
|
6014
6907
|
throw new Error(`Failed to init checkpoint repo: ${initResult.stderr}`);
|
|
@@ -6062,7 +6955,7 @@ async function createCheckpointManager(config) {
|
|
|
6062
6955
|
const changes = await manager.changes(checkpointId);
|
|
6063
6956
|
for (const change of changes.files) {
|
|
6064
6957
|
if (change.type === "added") {
|
|
6065
|
-
const filePath =
|
|
6958
|
+
const filePath = join6(workDir, change.path);
|
|
6066
6959
|
try {
|
|
6067
6960
|
await unlink3(filePath);
|
|
6068
6961
|
} catch {
|
|
@@ -6108,12 +7001,12 @@ async function createCheckpointManager(config) {
|
|
|
6108
7001
|
async undoFiles(checkpointId, files) {
|
|
6109
7002
|
await initialize();
|
|
6110
7003
|
for (const file of files) {
|
|
6111
|
-
const relativePath =
|
|
7004
|
+
const relativePath = relative3(workDir, resolve5(workDir, file));
|
|
6112
7005
|
const result = await git(gitDir, workDir, ["checkout", checkpointId, "--", relativePath]);
|
|
6113
7006
|
if (result.exitCode !== 0) {
|
|
6114
7007
|
const lsResult = await git(gitDir, workDir, ["ls-tree", checkpointId, "--", relativePath]);
|
|
6115
7008
|
if (!lsResult.stdout.trim()) {
|
|
6116
|
-
const filePath =
|
|
7009
|
+
const filePath = join6(workDir, relativePath);
|
|
6117
7010
|
try {
|
|
6118
7011
|
await unlink3(filePath);
|
|
6119
7012
|
} catch {
|
|
@@ -6147,7 +7040,7 @@ async function createCheckpointManager(config) {
|
|
|
6147
7040
|
},
|
|
6148
7041
|
async getFileAt(checkpointId, filePath) {
|
|
6149
7042
|
await initialize();
|
|
6150
|
-
const relativePath =
|
|
7043
|
+
const relativePath = relative3(workDir, resolve5(workDir, filePath));
|
|
6151
7044
|
const result = await git(gitDir, workDir, ["show", `${checkpointId}:${relativePath}`]);
|
|
6152
7045
|
if (result.exitCode !== 0) return null;
|
|
6153
7046
|
return result.stdout;
|
|
@@ -6169,6 +7062,23 @@ async function clearCheckpoints(workDir) {
|
|
|
6169
7062
|
}
|
|
6170
7063
|
}
|
|
6171
7064
|
|
|
7065
|
+
// src/middleware/approval.ts
|
|
7066
|
+
function approvalMiddleware(config = {}) {
|
|
7067
|
+
const handler = createApprovalHandler(config);
|
|
7068
|
+
return {
|
|
7069
|
+
name: "approval",
|
|
7070
|
+
async beforeToolCall(tool2, args, ctx) {
|
|
7071
|
+
try {
|
|
7072
|
+
await handler.request(ctx.sessionID, tool2, args, config.customRisks);
|
|
7073
|
+
return { action: "allow" };
|
|
7074
|
+
} catch (err) {
|
|
7075
|
+
const reason = err instanceof Error ? err.message : `Approval denied: ${tool2}`;
|
|
7076
|
+
return { action: "deny", reason };
|
|
7077
|
+
}
|
|
7078
|
+
}
|
|
7079
|
+
};
|
|
7080
|
+
}
|
|
7081
|
+
|
|
6172
7082
|
// src/mcp.ts
|
|
6173
7083
|
var mcpModule;
|
|
6174
7084
|
var stdioModule;
|
|
@@ -6432,6 +7342,583 @@ function sseServer(url, options) {
|
|
|
6432
7342
|
};
|
|
6433
7343
|
}
|
|
6434
7344
|
|
|
7345
|
+
// src/builtins/skill/tools.ts
|
|
7346
|
+
import { z } from "zod";
|
|
7347
|
+
function createSkillTool(registry) {
|
|
7348
|
+
return Tool.define("skill", () => {
|
|
7349
|
+
const skills = registry.list();
|
|
7350
|
+
const skillList = skills.length > 0 ? skills.map((s) => ` - "${s.name}": ${s.description}`).join("\n") : " (no skills available)";
|
|
7351
|
+
return {
|
|
7352
|
+
description: `Load a skill's full instructions to gain specialized knowledge for a task.
|
|
7353
|
+
|
|
7354
|
+
Available skills:
|
|
7355
|
+
${skillList}
|
|
7356
|
+
|
|
7357
|
+
Only load a skill when the current task clearly matches its description. Skills provide detailed workflows, reference material, and scripts.`,
|
|
7358
|
+
parameters: z.object({
|
|
7359
|
+
name: z.string().describe("The name of the skill to load (must match one of the available skills)")
|
|
7360
|
+
}),
|
|
7361
|
+
execute: async ({ name }) => {
|
|
7362
|
+
const content = await registry.loadContent(name);
|
|
7363
|
+
if (!content) {
|
|
7364
|
+
const available = registry.names.join(", ");
|
|
7365
|
+
return {
|
|
7366
|
+
title: `Skill not found: ${name}`,
|
|
7367
|
+
output: `No skill named "${name}" exists.
|
|
7368
|
+
|
|
7369
|
+
Available skills: ${available || "(none)"}`,
|
|
7370
|
+
metadata: {}
|
|
7371
|
+
};
|
|
7372
|
+
}
|
|
7373
|
+
const formatted = registry.formatContent(content);
|
|
7374
|
+
let depNote = "";
|
|
7375
|
+
if (content.dependencies && content.dependencies.length > 0) {
|
|
7376
|
+
const unloaded = content.dependencies.filter(
|
|
7377
|
+
(dep) => registry.has(dep) && !registry.isContentLoaded(dep)
|
|
7378
|
+
);
|
|
7379
|
+
if (unloaded.length > 0) {
|
|
7380
|
+
depNote = `
|
|
7381
|
+
|
|
7382
|
+
Note: This skill works best with these additional skills: ${unloaded.join(", ")}. Consider loading them if relevant.`;
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
return {
|
|
7386
|
+
title: `Loaded skill: ${content.name}`,
|
|
7387
|
+
output: formatted + depNote,
|
|
7388
|
+
metadata: {}
|
|
7389
|
+
};
|
|
7390
|
+
}
|
|
7391
|
+
};
|
|
7392
|
+
});
|
|
7393
|
+
}
|
|
7394
|
+
function createSkillResourceTool(registry) {
|
|
7395
|
+
return Tool.define("skill_resource", {
|
|
7396
|
+
description: "Read a specific file bundled with a skill (scripts, references, examples, assets). The skill must be loaded first via the `skill` tool to see available resources.",
|
|
7397
|
+
parameters: z.object({
|
|
7398
|
+
skill: z.string().describe("The skill name"),
|
|
7399
|
+
path: z.string().describe("Relative path to the resource file within the skill")
|
|
7400
|
+
}),
|
|
7401
|
+
execute: async ({ skill, path: path2 }) => {
|
|
7402
|
+
try {
|
|
7403
|
+
const content = await registry.loadResource(skill, path2);
|
|
7404
|
+
return {
|
|
7405
|
+
title: `${skill}/${path2}`,
|
|
7406
|
+
output: content,
|
|
7407
|
+
metadata: {}
|
|
7408
|
+
};
|
|
7409
|
+
} catch (err) {
|
|
7410
|
+
return {
|
|
7411
|
+
title: `Error reading resource`,
|
|
7412
|
+
output: err instanceof Error ? err.message : String(err),
|
|
7413
|
+
metadata: {}
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
}
|
|
7417
|
+
});
|
|
7418
|
+
}
|
|
7419
|
+
function createSkillTools(registry) {
|
|
7420
|
+
if (registry.size === 0) return [];
|
|
7421
|
+
return [
|
|
7422
|
+
createSkillTool(registry),
|
|
7423
|
+
createSkillResourceTool(registry)
|
|
7424
|
+
];
|
|
7425
|
+
}
|
|
7426
|
+
|
|
7427
|
+
// src/sub-agent/types.ts
|
|
7428
|
+
var DEFAULT_MAX_CONCURRENT = 6;
|
|
7429
|
+
var DEFAULT_MAX_SPAWN_DEPTH = 2;
|
|
7430
|
+
var DEFAULT_SESSION_TITLE_PREFIX = "Sub-agent";
|
|
7431
|
+
|
|
7432
|
+
// src/sub-agent/tracker.ts
|
|
7433
|
+
var SubAgentTracker = class {
|
|
7434
|
+
/** Active sub-agent handles by ID */
|
|
7435
|
+
handles = /* @__PURE__ */ new Map();
|
|
7436
|
+
/** Maximum concurrent sub-agents */
|
|
7437
|
+
maxConcurrent;
|
|
7438
|
+
/** Maximum nesting depth */
|
|
7439
|
+
maxDepth;
|
|
7440
|
+
/** Current depth in the spawn hierarchy */
|
|
7441
|
+
currentDepth;
|
|
7442
|
+
constructor(config) {
|
|
7443
|
+
this.maxConcurrent = config?.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
7444
|
+
this.maxDepth = config?.maxDepth ?? DEFAULT_MAX_SPAWN_DEPTH;
|
|
7445
|
+
this.currentDepth = config?.currentDepth ?? 0;
|
|
7446
|
+
}
|
|
7447
|
+
// ==========================================================================
|
|
7448
|
+
// Slot Management
|
|
7449
|
+
// ==========================================================================
|
|
7450
|
+
/** Number of currently active (running) sub-agents */
|
|
7451
|
+
get activeCount() {
|
|
7452
|
+
let count = 0;
|
|
7453
|
+
for (const handle of this.handles.values()) {
|
|
7454
|
+
if (handle.status.state === "running") count++;
|
|
7455
|
+
}
|
|
7456
|
+
return count;
|
|
7457
|
+
}
|
|
7458
|
+
/** Total tracked handles (including completed) */
|
|
7459
|
+
get totalCount() {
|
|
7460
|
+
return this.handles.size;
|
|
7461
|
+
}
|
|
7462
|
+
/**
|
|
7463
|
+
* Check if a new sub-agent can be spawned.
|
|
7464
|
+
* Returns an error message if not, undefined if OK.
|
|
7465
|
+
*/
|
|
7466
|
+
canSpawn() {
|
|
7467
|
+
if (this.currentDepth >= this.maxDepth) {
|
|
7468
|
+
return `Sub-agent depth limit reached (current: ${this.currentDepth}, max: ${this.maxDepth}). Complete the task yourself instead of delegating further.`;
|
|
7469
|
+
}
|
|
7470
|
+
if (this.activeCount >= this.maxConcurrent) {
|
|
7471
|
+
return `Maximum concurrent sub-agents reached (${this.maxConcurrent}). Wait for an existing sub-agent to complete before spawning a new one.`;
|
|
7472
|
+
}
|
|
7473
|
+
return void 0;
|
|
7474
|
+
}
|
|
7475
|
+
/** Whether the current depth allows further spawning */
|
|
7476
|
+
get canNest() {
|
|
7477
|
+
return this.currentDepth + 1 < this.maxDepth;
|
|
7478
|
+
}
|
|
7479
|
+
// ==========================================================================
|
|
7480
|
+
// Handle Management
|
|
7481
|
+
// ==========================================================================
|
|
7482
|
+
/** Register a new sub-agent handle */
|
|
7483
|
+
register(handle) {
|
|
7484
|
+
this.handles.set(handle.id, handle);
|
|
7485
|
+
}
|
|
7486
|
+
/** Get a handle by ID */
|
|
7487
|
+
get(id) {
|
|
7488
|
+
return this.handles.get(id);
|
|
7489
|
+
}
|
|
7490
|
+
/** Update a handle's status */
|
|
7491
|
+
updateStatus(id, status) {
|
|
7492
|
+
const handle = this.handles.get(id);
|
|
7493
|
+
if (handle) {
|
|
7494
|
+
handle.status = status;
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
/** Get all handles */
|
|
7498
|
+
list() {
|
|
7499
|
+
return Array.from(this.handles.values());
|
|
7500
|
+
}
|
|
7501
|
+
/** Get all running handles */
|
|
7502
|
+
running() {
|
|
7503
|
+
return this.list().filter((h) => h.status.state === "running");
|
|
7504
|
+
}
|
|
7505
|
+
/** Get all completed handles */
|
|
7506
|
+
completed() {
|
|
7507
|
+
return this.list().filter((h) => h.status.state === "completed");
|
|
7508
|
+
}
|
|
7509
|
+
/**
|
|
7510
|
+
* Cancel a running sub-agent.
|
|
7511
|
+
* Signals the abort controller and updates status.
|
|
7512
|
+
*/
|
|
7513
|
+
cancel(id) {
|
|
7514
|
+
const handle = this.handles.get(id);
|
|
7515
|
+
if (!handle || handle.status.state !== "running") return false;
|
|
7516
|
+
handle.abort.abort();
|
|
7517
|
+
handle.status = { state: "cancelled" };
|
|
7518
|
+
return true;
|
|
7519
|
+
}
|
|
7520
|
+
/**
|
|
7521
|
+
* Cancel all running sub-agents.
|
|
7522
|
+
*/
|
|
7523
|
+
cancelAll() {
|
|
7524
|
+
for (const handle of this.handles.values()) {
|
|
7525
|
+
if (handle.status.state === "running") {
|
|
7526
|
+
handle.abort.abort();
|
|
7527
|
+
handle.status = { state: "cancelled" };
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
/**
|
|
7532
|
+
* Wait for a specific sub-agent to complete.
|
|
7533
|
+
* Returns the result or throws if not found.
|
|
7534
|
+
*/
|
|
7535
|
+
async wait(id, timeoutMs) {
|
|
7536
|
+
const handle = this.handles.get(id);
|
|
7537
|
+
if (!handle) {
|
|
7538
|
+
throw new Error(`Sub-agent not found: "${id}"`);
|
|
7539
|
+
}
|
|
7540
|
+
if (handle.status.state === "completed") {
|
|
7541
|
+
return {
|
|
7542
|
+
response: handle.status.response,
|
|
7543
|
+
sessionId: handle.sessionId,
|
|
7544
|
+
usage: handle.status.usage,
|
|
7545
|
+
toolCalls: []
|
|
7546
|
+
};
|
|
7547
|
+
}
|
|
7548
|
+
if (handle.status.state !== "running") {
|
|
7549
|
+
throw new Error(
|
|
7550
|
+
`Sub-agent "${id}" is in state "${handle.status.state}" and cannot be waited on.`
|
|
7551
|
+
);
|
|
7552
|
+
}
|
|
7553
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
7554
|
+
const timeout = new Promise(
|
|
7555
|
+
(_, reject) => setTimeout(
|
|
7556
|
+
() => reject(new Error(`Timed out waiting for sub-agent "${id}" after ${timeoutMs}ms`)),
|
|
7557
|
+
timeoutMs
|
|
7558
|
+
)
|
|
7559
|
+
);
|
|
7560
|
+
return Promise.race([handle.promise, timeout]);
|
|
7561
|
+
}
|
|
7562
|
+
return handle.promise;
|
|
7563
|
+
}
|
|
7564
|
+
/**
|
|
7565
|
+
* Wait for any one of the given sub-agents to complete.
|
|
7566
|
+
* Returns the first result.
|
|
7567
|
+
*/
|
|
7568
|
+
async waitAny(ids, timeoutMs) {
|
|
7569
|
+
const handles = ids.map((id) => this.handles.get(id)).filter((h) => h !== void 0);
|
|
7570
|
+
if (handles.length === 0) {
|
|
7571
|
+
throw new Error(`No sub-agents found for IDs: ${ids.join(", ")}`);
|
|
7572
|
+
}
|
|
7573
|
+
for (const handle of handles) {
|
|
7574
|
+
if (handle.status.state === "completed") {
|
|
7575
|
+
return {
|
|
7576
|
+
id: handle.id,
|
|
7577
|
+
result: {
|
|
7578
|
+
response: handle.status.response,
|
|
7579
|
+
sessionId: handle.sessionId,
|
|
7580
|
+
usage: handle.status.usage,
|
|
7581
|
+
toolCalls: []
|
|
7582
|
+
}
|
|
7583
|
+
};
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
const races = handles.filter((h) => h.status.state === "running").map(
|
|
7587
|
+
(h) => h.promise.then((result) => ({ id: h.id, result }))
|
|
7588
|
+
);
|
|
7589
|
+
if (races.length === 0) {
|
|
7590
|
+
throw new Error("No running sub-agents to wait on.");
|
|
7591
|
+
}
|
|
7592
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
7593
|
+
const timeout = new Promise(
|
|
7594
|
+
(resolve6) => setTimeout(() => resolve6({ timedOut: true }), timeoutMs)
|
|
7595
|
+
);
|
|
7596
|
+
return Promise.race([...races, timeout]);
|
|
7597
|
+
}
|
|
7598
|
+
return Promise.race(races);
|
|
7599
|
+
}
|
|
7600
|
+
/**
|
|
7601
|
+
* Create a child tracker configuration for a nested sub-agent.
|
|
7602
|
+
* Increments the depth counter.
|
|
7603
|
+
*/
|
|
7604
|
+
childConfig() {
|
|
7605
|
+
return {
|
|
7606
|
+
maxConcurrent: this.maxConcurrent,
|
|
7607
|
+
maxDepth: this.maxDepth,
|
|
7608
|
+
currentDepth: this.currentDepth + 1
|
|
7609
|
+
};
|
|
7610
|
+
}
|
|
7611
|
+
};
|
|
7612
|
+
|
|
7613
|
+
// src/builtins/sub-agent/tools.ts
|
|
7614
|
+
import { z as z2 } from "zod";
|
|
7615
|
+
var agentCounter = 0;
|
|
7616
|
+
function generateAgentId(profileName) {
|
|
7617
|
+
return `${profileName}-${++agentCounter}-${Date.now().toString(36)}`;
|
|
7618
|
+
}
|
|
7619
|
+
function createInvokeAgentTool(parent, config, tracker) {
|
|
7620
|
+
const isAsync = config.async ?? false;
|
|
7621
|
+
const titlePrefix = config.sessionTitlePrefix ?? DEFAULT_SESSION_TITLE_PREFIX;
|
|
7622
|
+
return Tool.define("invoke_agent", () => {
|
|
7623
|
+
const profileList = config.profiles.map((p) => ` - "${p.name}": ${p.description}`).join("\n");
|
|
7624
|
+
const validNames = config.profiles.map((p) => p.name);
|
|
7625
|
+
const modeNote = isAsync ? "Returns an agent ID immediately. Use `wait_agent` to collect the result." : "Blocks until the sub-agent completes and returns the result directly.";
|
|
7626
|
+
return {
|
|
7627
|
+
description: `Delegate a focused task to a specialized sub-agent.
|
|
7628
|
+
|
|
7629
|
+
Available agent types:
|
|
7630
|
+
${profileList}
|
|
7631
|
+
|
|
7632
|
+
${modeNote}
|
|
7633
|
+
|
|
7634
|
+
Guidelines:
|
|
7635
|
+
- Use sub-agents for well-scoped, independent tasks
|
|
7636
|
+
- Provide clear, self-contained instructions \u2014 the sub-agent has a fresh context
|
|
7637
|
+
- Include any relevant file paths, function names, or context the sub-agent needs
|
|
7638
|
+
- Mention the working directory structure if known (e.g. "code is in packages/agent-core/src")
|
|
7639
|
+
- For parallel work, invoke multiple agents before waiting for results
|
|
7640
|
+
- Sub-agents will retry on errors and explore alternatives \u2014 they are resilient`,
|
|
7641
|
+
parameters: z2.object({
|
|
7642
|
+
agent_type: z2.string().describe(
|
|
7643
|
+
`The type of specialized agent to use. Must be one of: ${validNames.join(", ")}`
|
|
7644
|
+
),
|
|
7645
|
+
task: z2.string().describe(
|
|
7646
|
+
"A detailed, self-contained description of the task. Include all context the sub-agent needs."
|
|
7647
|
+
),
|
|
7648
|
+
title: z2.string().optional().describe("Short title for tracking (3-5 words)")
|
|
7649
|
+
}),
|
|
7650
|
+
execute: async (params, ctx) => {
|
|
7651
|
+
const profile = config.profiles.find((p) => p.name === params.agent_type);
|
|
7652
|
+
if (!profile) {
|
|
7653
|
+
return {
|
|
7654
|
+
title: "Invalid agent type",
|
|
7655
|
+
output: `Unknown agent type "${params.agent_type}".
|
|
7656
|
+
|
|
7657
|
+
Available types: ${validNames.join(", ")}`,
|
|
7658
|
+
metadata: {}
|
|
7659
|
+
};
|
|
7660
|
+
}
|
|
7661
|
+
const spawnError = tracker.canSpawn();
|
|
7662
|
+
if (spawnError) {
|
|
7663
|
+
return {
|
|
7664
|
+
title: "Cannot spawn sub-agent",
|
|
7665
|
+
output: spawnError,
|
|
7666
|
+
metadata: {}
|
|
7667
|
+
};
|
|
7668
|
+
}
|
|
7669
|
+
const child = buildChildAgent(parent, profile);
|
|
7670
|
+
const taskTitle = params.title ?? `${titlePrefix}: ${profile.name}`;
|
|
7671
|
+
if (isAsync) {
|
|
7672
|
+
return runAsync(child, profile, params.task, taskTitle, { sessionId: ctx.sessionID, abort: ctx.abort }, tracker);
|
|
7673
|
+
} else {
|
|
7674
|
+
return runSync(child, profile, params.task, taskTitle, { sessionId: ctx.sessionID, abort: ctx.abort });
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
};
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
7680
|
+
async function runSync(child, profile, task, title, ctx) {
|
|
7681
|
+
try {
|
|
7682
|
+
const result = await child.run({
|
|
7683
|
+
parentSessionId: ctx.sessionId,
|
|
7684
|
+
message: task,
|
|
7685
|
+
title,
|
|
7686
|
+
abort: ctx.abort
|
|
7687
|
+
});
|
|
7688
|
+
const usageLine = result.usage ? `
|
|
7689
|
+
Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out` : "";
|
|
7690
|
+
const toolsLine = result.toolCalls.length > 0 ? `
|
|
7691
|
+
Tool calls: ${result.toolCalls.map((t) => t.name).join(", ")}` : "";
|
|
7692
|
+
return {
|
|
7693
|
+
title: `${profile.name} completed`,
|
|
7694
|
+
output: `<agent_result agent="${profile.name}" session="${result.sessionId}">
|
|
7695
|
+
` + result.response + `
|
|
7696
|
+
</agent_result>` + usageLine + toolsLine,
|
|
7697
|
+
metadata: {
|
|
7698
|
+
sessionId: result.sessionId,
|
|
7699
|
+
profile: profile.name,
|
|
7700
|
+
toolCalls: result.toolCalls.length
|
|
7701
|
+
}
|
|
7702
|
+
};
|
|
7703
|
+
} catch (err) {
|
|
7704
|
+
return {
|
|
7705
|
+
title: `${profile.name} failed`,
|
|
7706
|
+
output: `Sub-agent "${profile.name}" encountered an error:
|
|
7707
|
+
` + (err instanceof Error ? err.message : String(err)) + `
|
|
7708
|
+
|
|
7709
|
+
Consider handling the task directly.`,
|
|
7710
|
+
metadata: { profile: profile.name, error: true }
|
|
7711
|
+
};
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
async function runAsync(child, profile, task, title, ctx, tracker) {
|
|
7715
|
+
const id = generateAgentId(profile.name);
|
|
7716
|
+
const sessionId = ctx.sessionId ? `${ctx.sessionId}:sub:${id}` : `sub:${id}`;
|
|
7717
|
+
const abortController = new AbortController();
|
|
7718
|
+
if (ctx.abort) {
|
|
7719
|
+
ctx.abort.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
7720
|
+
}
|
|
7721
|
+
const promise = child.run({
|
|
7722
|
+
parentSessionId: ctx.sessionId,
|
|
7723
|
+
message: task,
|
|
7724
|
+
title,
|
|
7725
|
+
abort: abortController.signal
|
|
7726
|
+
}).then((result) => {
|
|
7727
|
+
const completed = {
|
|
7728
|
+
response: result.response,
|
|
7729
|
+
sessionId: result.sessionId,
|
|
7730
|
+
usage: {
|
|
7731
|
+
inputTokens: result.usage?.inputTokens ?? 0,
|
|
7732
|
+
outputTokens: result.usage?.outputTokens ?? 0,
|
|
7733
|
+
totalTokens: result.usage?.totalTokens ?? 0
|
|
7734
|
+
},
|
|
7735
|
+
toolCalls: result.toolCalls
|
|
7736
|
+
};
|
|
7737
|
+
tracker.updateStatus(id, {
|
|
7738
|
+
state: "completed",
|
|
7739
|
+
response: result.response,
|
|
7740
|
+
usage: completed.usage
|
|
7741
|
+
});
|
|
7742
|
+
return completed;
|
|
7743
|
+
}).catch((err) => {
|
|
7744
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
7745
|
+
tracker.updateStatus(id, { state: "errored", error: errorMsg });
|
|
7746
|
+
throw err;
|
|
7747
|
+
});
|
|
7748
|
+
const handle = {
|
|
7749
|
+
id,
|
|
7750
|
+
name: profile.name,
|
|
7751
|
+
sessionId,
|
|
7752
|
+
status: { state: "running" },
|
|
7753
|
+
promise,
|
|
7754
|
+
abort: abortController
|
|
7755
|
+
};
|
|
7756
|
+
tracker.register(handle);
|
|
7757
|
+
return {
|
|
7758
|
+
title: `Spawned ${profile.name}`,
|
|
7759
|
+
output: `Sub-agent spawned successfully.
|
|
7760
|
+
|
|
7761
|
+
Agent ID: ${id}
|
|
7762
|
+
Type: ${profile.name}
|
|
7763
|
+
Session: ${sessionId}
|
|
7764
|
+
|
|
7765
|
+
Use \`wait_agent\` with this ID to collect the result when ready.`,
|
|
7766
|
+
metadata: {
|
|
7767
|
+
agentId: id,
|
|
7768
|
+
profile: profile.name,
|
|
7769
|
+
sessionId
|
|
7770
|
+
}
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
function createWaitAgentTool(tracker) {
|
|
7774
|
+
return Tool.define("wait_agent", {
|
|
7775
|
+
description: "Wait for one or more sub-agents to complete and return their results. When multiple IDs are provided, returns whichever finishes first. Use a timeout to avoid blocking indefinitely.",
|
|
7776
|
+
parameters: z2.object({
|
|
7777
|
+
ids: z2.array(z2.string()).min(1).describe("Agent IDs to wait on (from invoke_agent)"),
|
|
7778
|
+
timeout_ms: z2.number().optional().describe("Timeout in milliseconds (default: 60000, max: 600000)")
|
|
7779
|
+
}),
|
|
7780
|
+
execute: async (params) => {
|
|
7781
|
+
const timeoutMs = Math.min(params.timeout_ms ?? 6e4, 6e5);
|
|
7782
|
+
const missing = params.ids.filter((id) => !tracker.get(id));
|
|
7783
|
+
if (missing.length > 0) {
|
|
7784
|
+
return {
|
|
7785
|
+
title: "Agent(s) not found",
|
|
7786
|
+
output: `Unknown agent IDs: ${missing.join(", ")}`,
|
|
7787
|
+
metadata: {}
|
|
7788
|
+
};
|
|
7789
|
+
}
|
|
7790
|
+
try {
|
|
7791
|
+
if (params.ids.length === 1) {
|
|
7792
|
+
const result = await tracker.wait(params.ids[0], timeoutMs);
|
|
7793
|
+
return formatWaitResult(params.ids[0], result, tracker);
|
|
7794
|
+
} else {
|
|
7795
|
+
const outcome = await tracker.waitAny(params.ids, timeoutMs);
|
|
7796
|
+
if ("timedOut" in outcome) {
|
|
7797
|
+
const running = params.ids.map((id) => tracker.get(id)).filter((h) => h?.status.state === "running").map((h) => h.id);
|
|
7798
|
+
return {
|
|
7799
|
+
title: "Wait timed out",
|
|
7800
|
+
output: `Timed out after ${timeoutMs}ms. Still running: ${running.join(", ")}.
|
|
7801
|
+
|
|
7802
|
+
Call \`wait_agent\` again to continue waiting, or \`close_agent\` to cancel.`,
|
|
7803
|
+
metadata: { timedOut: true }
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
return formatWaitResult(outcome.id, outcome.result, tracker);
|
|
7807
|
+
}
|
|
7808
|
+
} catch (err) {
|
|
7809
|
+
return {
|
|
7810
|
+
title: "Wait failed",
|
|
7811
|
+
output: err instanceof Error ? err.message : String(err),
|
|
7812
|
+
metadata: { error: true }
|
|
7813
|
+
};
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
});
|
|
7817
|
+
}
|
|
7818
|
+
function formatWaitResult(id, result, tracker) {
|
|
7819
|
+
const handle = tracker.get(id);
|
|
7820
|
+
const profileName = handle?.name ?? "agent";
|
|
7821
|
+
const usageLine = result.usage ? `
|
|
7822
|
+
Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out` : "";
|
|
7823
|
+
const toolsLine = result.toolCalls.length > 0 ? `
|
|
7824
|
+
Tool calls: ${result.toolCalls.map((t) => t.name).join(", ")}` : "";
|
|
7825
|
+
const stillRunning = tracker.running().filter((h) => h.id !== id);
|
|
7826
|
+
const runningNote = stillRunning.length > 0 ? `
|
|
7827
|
+
|
|
7828
|
+
Still running: ${stillRunning.map((h) => `${h.id} (${h.name})`).join(", ")}` : "";
|
|
7829
|
+
return {
|
|
7830
|
+
title: `${profileName} completed`,
|
|
7831
|
+
output: `<agent_result agent="${profileName}" id="${id}" session="${result.sessionId}">
|
|
7832
|
+
` + result.response + `
|
|
7833
|
+
</agent_result>` + usageLine + toolsLine + runningNote,
|
|
7834
|
+
metadata: {
|
|
7835
|
+
agentId: id,
|
|
7836
|
+
profile: profileName,
|
|
7837
|
+
sessionId: result.sessionId,
|
|
7838
|
+
toolCalls: result.toolCalls.length
|
|
7839
|
+
}
|
|
7840
|
+
};
|
|
7841
|
+
}
|
|
7842
|
+
function createCloseAgentTool(tracker) {
|
|
7843
|
+
return Tool.define("close_agent", {
|
|
7844
|
+
description: "Cancel a running sub-agent. Use when the result is no longer needed or the sub-agent is taking too long.",
|
|
7845
|
+
parameters: z2.object({
|
|
7846
|
+
id: z2.string().describe("Agent ID to cancel (from invoke_agent)")
|
|
7847
|
+
}),
|
|
7848
|
+
execute: async (params) => {
|
|
7849
|
+
const handle = tracker.get(params.id);
|
|
7850
|
+
if (!handle) {
|
|
7851
|
+
return {
|
|
7852
|
+
title: "Agent not found",
|
|
7853
|
+
output: `No sub-agent with ID "${params.id}".`,
|
|
7854
|
+
metadata: {}
|
|
7855
|
+
};
|
|
7856
|
+
}
|
|
7857
|
+
if (handle.status.state !== "running") {
|
|
7858
|
+
return {
|
|
7859
|
+
title: `Agent already ${handle.status.state}`,
|
|
7860
|
+
output: `Sub-agent "${params.id}" is already ${handle.status.state}. ` + (handle.status.state === "completed" ? "Use `wait_agent` to retrieve its result." : "No action taken."),
|
|
7861
|
+
metadata: { state: handle.status.state }
|
|
7862
|
+
};
|
|
7863
|
+
}
|
|
7864
|
+
tracker.cancel(params.id);
|
|
7865
|
+
return {
|
|
7866
|
+
title: `Cancelled ${handle.name}`,
|
|
7867
|
+
output: `Sub-agent "${params.id}" (${handle.name}) has been cancelled.`,
|
|
7868
|
+
metadata: {
|
|
7869
|
+
agentId: params.id,
|
|
7870
|
+
profile: handle.name
|
|
7871
|
+
}
|
|
7872
|
+
};
|
|
7873
|
+
}
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7876
|
+
function buildChildAgent(parent, profile) {
|
|
7877
|
+
const forkOptions = {};
|
|
7878
|
+
if (profile.preset) {
|
|
7879
|
+
forkOptions.preset = profile.preset;
|
|
7880
|
+
}
|
|
7881
|
+
if (profile.systemPrompt) {
|
|
7882
|
+
forkOptions.systemPrompt = profile.systemPrompt;
|
|
7883
|
+
}
|
|
7884
|
+
if (profile.model) {
|
|
7885
|
+
forkOptions.model = profile.model;
|
|
7886
|
+
}
|
|
7887
|
+
if (profile.maxSteps) {
|
|
7888
|
+
forkOptions.maxSteps = profile.maxSteps;
|
|
7889
|
+
}
|
|
7890
|
+
if (profile.additionalMiddleware) {
|
|
7891
|
+
forkOptions.additionalMiddleware = profile.additionalMiddleware;
|
|
7892
|
+
}
|
|
7893
|
+
const child = parent.fork(forkOptions);
|
|
7894
|
+
if (profile.additionalTools && profile.additionalTools.length > 0) {
|
|
7895
|
+
const currentTools = child.getTools();
|
|
7896
|
+
return parent.fork({
|
|
7897
|
+
...forkOptions,
|
|
7898
|
+
tools: [...currentTools, ...profile.additionalTools]
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
return child;
|
|
7902
|
+
}
|
|
7903
|
+
function createSubAgentTools(parent, config) {
|
|
7904
|
+
if (config.profiles.length === 0) {
|
|
7905
|
+
return [];
|
|
7906
|
+
}
|
|
7907
|
+
const currentDepth = config.currentDepth ?? 0;
|
|
7908
|
+
const maxDepth = config.maxDepth ?? DEFAULT_MAX_SPAWN_DEPTH;
|
|
7909
|
+
if (currentDepth >= maxDepth) {
|
|
7910
|
+
return [];
|
|
7911
|
+
}
|
|
7912
|
+
const tracker = new SubAgentTracker(config);
|
|
7913
|
+
const tools = [];
|
|
7914
|
+
tools.push(createInvokeAgentTool(parent, config, tracker));
|
|
7915
|
+
if (config.async) {
|
|
7916
|
+
tools.push(createWaitAgentTool(tracker));
|
|
7917
|
+
tools.push(createCloseAgentTool(tracker));
|
|
7918
|
+
}
|
|
7919
|
+
return tools;
|
|
7920
|
+
}
|
|
7921
|
+
|
|
6435
7922
|
// src/models/resolver.ts
|
|
6436
7923
|
function parseKey(input) {
|
|
6437
7924
|
const [engineId, ...rest] = input.split("/");
|
|
@@ -6551,10 +8038,16 @@ export {
|
|
|
6551
8038
|
ContextManager,
|
|
6552
8039
|
ContextOverflowError,
|
|
6553
8040
|
DEFAULT_CONTEXT_LIMITS,
|
|
8041
|
+
DEFAULT_EXTERNAL_DIRS,
|
|
6554
8042
|
DEFAULT_INSTRUCTION_PATTERNS,
|
|
8043
|
+
DEFAULT_MAX_CONCURRENT,
|
|
6555
8044
|
DEFAULT_MAX_DEPTH,
|
|
6556
8045
|
DEFAULT_MAX_FILE_SIZE,
|
|
8046
|
+
DEFAULT_MAX_SCAN_DEPTH,
|
|
8047
|
+
DEFAULT_MAX_SPAWN_DEPTH,
|
|
6557
8048
|
DEFAULT_RETRY_CONFIG,
|
|
8049
|
+
DEFAULT_SESSION_TITLE_PREFIX,
|
|
8050
|
+
DEFAULT_SKILL_MAX_SIZE,
|
|
6558
8051
|
DoomLoopError,
|
|
6559
8052
|
FileStorage,
|
|
6560
8053
|
InterventionController,
|
|
@@ -6563,6 +8056,7 @@ export {
|
|
|
6563
8056
|
MAX_BYTES,
|
|
6564
8057
|
MAX_LINES,
|
|
6565
8058
|
MemoryStorage,
|
|
8059
|
+
MiddlewareRunner,
|
|
6566
8060
|
ModelCapabilityResolver,
|
|
6567
8061
|
OUTPUT_TOKEN_MAX,
|
|
6568
8062
|
PRIORITY_BASE,
|
|
@@ -6573,14 +8067,18 @@ export {
|
|
|
6573
8067
|
PRIORITY_SKILLS,
|
|
6574
8068
|
Presets,
|
|
6575
8069
|
PromptBuilder,
|
|
8070
|
+
SKILL_FILENAME,
|
|
6576
8071
|
STORAGE_VERSION,
|
|
6577
8072
|
SessionManager,
|
|
8073
|
+
SkillRegistry,
|
|
8074
|
+
SubAgentTracker,
|
|
6578
8075
|
TRUNCATE_DIR,
|
|
6579
8076
|
TRUNCATE_GLOB,
|
|
6580
8077
|
Tool,
|
|
6581
8078
|
ToolRegistry,
|
|
6582
8079
|
TurnChangeTracker,
|
|
6583
8080
|
applyPreset,
|
|
8081
|
+
approvalMiddleware,
|
|
6584
8082
|
buildMessagesFromEntries,
|
|
6585
8083
|
buildReasoningOptions,
|
|
6586
8084
|
buildReasoningOptionsSync,
|
|
@@ -6597,6 +8095,11 @@ export {
|
|
|
6597
8095
|
createResolver,
|
|
6598
8096
|
createRetryHandler,
|
|
6599
8097
|
createRetryState,
|
|
8098
|
+
createSkillRegistry,
|
|
8099
|
+
createSkillResourceTool,
|
|
8100
|
+
createSkillTool,
|
|
8101
|
+
createSkillTools,
|
|
8102
|
+
createSubAgentTools,
|
|
6600
8103
|
createTurnTracker,
|
|
6601
8104
|
defaultRegistry,
|
|
6602
8105
|
defineServer,
|
|
@@ -6604,7 +8107,9 @@ export {
|
|
|
6604
8107
|
deserializeMessage,
|
|
6605
8108
|
detectModelFamily,
|
|
6606
8109
|
discoverInstructions,
|
|
8110
|
+
discoverSkills,
|
|
6607
8111
|
dockerHost,
|
|
8112
|
+
emptySkillRegistry,
|
|
6608
8113
|
estimateConversationTokens,
|
|
6609
8114
|
estimateMessageTokens,
|
|
6610
8115
|
estimateTokens,
|
|
@@ -6634,12 +8139,17 @@ export {
|
|
|
6634
8139
|
getTemplate,
|
|
6635
8140
|
getToolRisk,
|
|
6636
8141
|
httpServer,
|
|
8142
|
+
inferResourceType,
|
|
6637
8143
|
isContextOverflowing,
|
|
6638
8144
|
isRetryable,
|
|
6639
8145
|
isRetryableCategory,
|
|
6640
8146
|
loadGlobalInstructions,
|
|
8147
|
+
loadResourceContent,
|
|
8148
|
+
loadSkillContent,
|
|
8149
|
+
loadSkillMetadata,
|
|
6641
8150
|
localHost,
|
|
6642
8151
|
mergePresets,
|
|
8152
|
+
parseFrontmatter,
|
|
6643
8153
|
parseJSONL,
|
|
6644
8154
|
processStream,
|
|
6645
8155
|
pruneContext,
|