@cuylabs/agent-core 0.2.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/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((resolve5) => {
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", (code) => {
1184
- this.gitDetected = code === 0;
1185
- resolve5(this.gitDetected);
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
- resolve5(false);
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((resolve5, reject) => {
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
- resolve5();
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((resolve5, reject) => {
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", (code) => {
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
- resolve5({ stdout, stderr, exitCode: code, timedOut });
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((resolve5, reject) => {
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
- resolve5({
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
- resolve5({
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((resolve5) => {
5101
- resolveNext = resolve5;
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 dir = a.path.substring(0, a.path.lastIndexOf("/") + 1);
5741
- return [dir ? `${dir}*` : a.path];
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((resolve5, reject) => {
5847
- pending.set(id, { resolve: resolve5, reject });
5848
- onRequest(request2).then(resolve5).catch(reject).finally(() => pending.delete(id));
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 readFile4, writeFile as writeFile3, access as access2, rm } from "fs/promises";
5896
- import { join as join4, relative as relative2, resolve as resolve4, dirname as dirname3 } from "path";
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 homedir4 } from "os";
6791
+ import { homedir as homedir5 } from "os";
5899
6792
  async function git(gitDir, workTree, args, _options = {}) {
5900
- return new Promise((resolve5) => {
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", (code) => {
5920
- const exitCode = code ?? 0;
5921
- resolve5({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode });
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
- resolve5({ stdout: "", stderr: err.message, exitCode: 1 });
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 join4(homedir4(), ".cuylabs", "checkpoints", id);
6826
+ return join6(homedir5(), ".cuylabs", "checkpoints", id);
5934
6827
  }
5935
6828
  async function readCheckpointLog(logPath) {
5936
6829
  try {
5937
- const content = await readFile4(logPath, "utf-8");
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(dirname3(logPath), { recursive: true });
6848
+ await mkdir3(dirname4(logPath), { recursive: true });
5956
6849
  try {
5957
- const existing = await readFile4(logPath, "utf-8");
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 readFile4(logPath, "utf-8");
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 = resolve4(config.workDir);
6869
+ const workDir = resolve5(config.workDir);
5977
6870
  const storageDir = config.storageDir ?? defaultStorageDir(workDir);
5978
- const gitDir = join4(storageDir, "git");
5979
- const logPath = join4(storageDir, "checkpoints.jsonl");
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 = join4(gitDir, "HEAD");
6888
+ const headPath = join6(gitDir, "HEAD");
5996
6889
  let needsInit = false;
5997
6890
  try {
5998
- await access2(headPath);
6891
+ await access3(headPath);
5999
6892
  } catch {
6000
6893
  needsInit = true;
6001
6894
  }
6002
6895
  if (needsInit) {
6003
- const initResult = await new Promise((resolve5) => {
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", (code) => resolve5({ exitCode: code ?? 0, stderr }));
6011
- proc.on("error", (err) => resolve5({ exitCode: 1, stderr: err.message }));
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 = join4(workDir, change.path);
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 = relative2(workDir, resolve4(workDir, file));
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 = join4(workDir, relativePath);
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 = relative2(workDir, resolve4(workDir, filePath));
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,