[GH-ISSUE #387] skillsMiddleware skips skill.md discovery for explicitly scoped skill directories #252

Open
opened 2026-06-05 17:21:17 -04:00 by yindo · 1 comment
Owner

Originally created by @lullabyjune on GitHub (Mar 29, 2026).
Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/387

Originally assigned to: @colifran on GitHub.

Description

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title.
  • I searched existing issues and didn't find this.
  • I can reproduce this with the latest released version.
  • I included a minimal reproducible example and steps to reproduce.

Area (Required)

  • deepagents (SDK)

Related Issues / PRs

No response

Reproduction Steps / Example Code

import { createDeepAgent } from "deepagents";

const agent = createDeepAgent({
  model,
  tools: [],
  skills: ["skills/domainA/"],
});

Skill layout:

skills/
  domainA/
    skill.md

Error Message and Stack Trace (if applicable)

No runtime error is thrown. The issue is that skillsMiddleware fails to inject the metadata from skills/domainA/skill.md into the final systemPrompt.

Description

When skills is configured with a specific skill directory path such as:

skills: ["skills/domainA/"]

skillsMiddleware does not correctly discover and inject the metadata from skills/domainA/skill.md into the final systemPrompt.

I traced this to the handling around:

  • libs/deepagents/src/middleware/skills.ts#L527

When the input is a specific directory path like skills/domainA/, the resolved entry is treated as a file rather than a directory, and the code immediately continues. As a result, the domainA/skill.md sniffing logic is skipped entirely.

I believe this is a bug rather than intended behavior for several reasons:

  1. At:

    • libs/deepagents/src/middleware/skills.ts#L715

    after replacement, skills_locations is populated, but skills_list becomes none. So the final prompt is internally inconsistent: it effectively says the agent has access to one or more skill locations, but the available skills list is none.

  2. At:

    • libs/deepagents/src/middleware/skills.ts#L148

    the code/comments explicitly indicate that passing a specific directory under skills, such as /skills/user/, is a supported case.

  3. In production, assigning a sub-agent access to a specific skill rather than exposing all global skills is a real and important use case.

Expected behavior

If skills includes a specific skill directory like:

skills: ["skills/domainA/"]

then skillsMiddleware should still detect skills/domainA/skill.md, parse its metadata, and include that skill in the generated skills_list and injected systemPrompt.

Actual behavior

skills_locations is populated, but skills_list becomes none, and the metadata from skills/domainA/skill.md is not injected into the final systemPrompt.

Suggested direction

It looks like the logic around skills.ts#L527 should treat an explicitly provided skill directory path as a directory-like skill root and still attempt skill.md discovery, instead of skipping it when the resolved entry is not handled as expected.

Additional context

If maintainers confirm this is a bug rather than intended behavior, I’m happy to contribute a PR to fix it.

Environment / System Info

  • deepagents: latest released version at time of writing
  • Node.js: 20.x
  • Platform: reproducible across environments; this appears to be middleware path-handling behavior rather than OS-specific behavior
Originally created by @lullabyjune on GitHub (Mar 29, 2026). Original GitHub issue: https://github.com/langchain-ai/deepagentsjs/issues/387 Originally assigned to: @colifran on GitHub. ## Description ### Checked other resources - This is a bug, not a usage question. - I added a clear and descriptive title. - I searched existing issues and didn't find this. - I can reproduce this with the latest released version. - I included a minimal reproducible example and steps to reproduce. ### Area (Required) - deepagents (SDK) ### Related Issues / PRs No response ### Reproduction Steps / Example Code ```ts import { createDeepAgent } from "deepagents"; const agent = createDeepAgent({ model, tools: [], skills: ["skills/domainA/"], }); ``` Skill layout: ```text skills/ domainA/ skill.md ``` ### Error Message and Stack Trace (if applicable) No runtime error is thrown. The issue is that `skillsMiddleware` fails to inject the metadata from `skills/domainA/skill.md` into the final `systemPrompt`. ### Description When `skills` is configured with a specific skill directory path such as: ```ts skills: ["skills/domainA/"] ``` `skillsMiddleware` does not correctly discover and inject the metadata from `skills/domainA/skill.md` into the final `systemPrompt`. I traced this to the handling around: - `libs/deepagents/src/middleware/skills.ts#L527` When the input is a specific directory path like `skills/domainA/`, the resolved `entry` is treated as a file rather than a directory, and the code immediately `continue`s. As a result, the `domainA/skill.md` sniffing logic is skipped entirely. I believe this is a bug rather than intended behavior for several reasons: 1. At: - `libs/deepagents/src/middleware/skills.ts#L715` after replacement, `skills_locations` is populated, but `skills_list` becomes `none`. So the final prompt is internally inconsistent: it effectively says the agent has access to one or more skill locations, but the available skills list is `none`. 2. At: - `libs/deepagents/src/middleware/skills.ts#L148` the code/comments explicitly indicate that passing a specific directory under `skills`, such as `/skills/user/`, is a supported case. 3. In production, assigning a sub-agent access to a specific skill rather than exposing all global skills is a real and important use case. ### Expected behavior If `skills` includes a specific skill directory like: ```ts skills: ["skills/domainA/"] ``` then `skillsMiddleware` should still detect `skills/domainA/skill.md`, parse its metadata, and include that skill in the generated `skills_list` and injected `systemPrompt`. ### Actual behavior `skills_locations` is populated, but `skills_list` becomes `none`, and the metadata from `skills/domainA/skill.md` is not injected into the final `systemPrompt`. ### Suggested direction It looks like the logic around `skills.ts#L527` should treat an explicitly provided skill directory path as a directory-like skill root and still attempt `skill.md` discovery, instead of skipping it when the resolved entry is not handled as expected. ### Additional context If maintainers confirm this is a bug rather than intended behavior, I’m happy to contribute a PR to fix it. ### Environment / System Info - deepagents: latest released version at time of writing - Node.js: 20.x - Platform: reproducible across environments; this appears to be middleware path-handling behavior rather than OS-specific behavior
Author
Owner

@thushanthbengre22-dev commented on GitHub (Apr 14, 2026):

Confirmed Bug - Root Cause Identified

I've traced through the code and can confirm this is a bug. Here's the exact flow:

How listSkillsFromBackend (line 493) works:

  1. Calls ls(normalizedPath) to list contents of the given source path
  2. Iterates results, skipping anything that isn't a directory (line 536)
  3. For each subdirectory found, looks for <subdirName>/SKILL.md

The Bug - When source is skills/domainA/:

  • ls("skills/domainA/") returns the contents of that directory (e.g., SKILL.md), not a subdirectory named domainA
  • The loop at line 535 skips everything that isn't a directory, so SKILL.md is ignored entirely
  • Result: loadedSkills is empty → formatSkillsList() returns nothing → {skills_list} in the prompt is blank
  • However, formatSkillsLocations(sources) still lists skills/domainA/ as a location
  • This creates the inconsistency noted at line 715: the prompt says the agent has access to skill locations, but the available skills list is empty

Root Cause:

The function only understands source paths as parent containers of skill directories. It has no logic to detect that the source path itself is a skill directory (i.e., that SKILL.md might live directly at skills/domainA/SKILL.md when the source is skills/domainA/).

The comment at line 151 confirms this is unintended behavior — passing a specific skill directory like /skills/user/ is documented as a supported pattern.

Proposed Fix:

After listing the contents, check if a SKILL.md exists directly at normalizedPath itself. If so, treat the source as a single skill directory and parse its metadata directly.

Can I work on this?

I'd like to submit a PR to fix this issue. The approach would be:

  1. Before the directory iteration loop, check if SKILL.md exists at the source path itself
  2. If it does, parse it as a single skill and add to loadedSkills
  3. Otherwise, continue with the existing subdirectory discovery logic

Let me know if anyone else is working on this or if there's a preferred approach. Happy to collaborate!

<!-- gh-comment-id:4247250956 --> @thushanthbengre22-dev commented on GitHub (Apr 14, 2026): ## Confirmed Bug - Root Cause Identified I've traced through the code and can confirm this is a bug. Here's the exact flow: ### How `listSkillsFromBackend` (line 493) works: 1. Calls `ls(normalizedPath)` to list contents of the given source path 2. Iterates results, skipping anything that isn't a directory (line 536) 3. For each subdirectory found, looks for `<subdirName>/SKILL.md` ### The Bug - When source is `skills/domainA/`: - `ls("skills/domainA/")` returns the **contents** of that directory (e.g., `SKILL.md`), not a subdirectory named `domainA` - The loop at line 535 skips everything that isn't a directory, so `SKILL.md` is ignored entirely - **Result:** `loadedSkills` is empty → `formatSkillsList()` returns nothing → `{skills_list}` in the prompt is blank - However, `formatSkillsLocations(sources)` still lists `skills/domainA/` as a location - This creates the inconsistency noted at line 715: the prompt says the agent has access to skill locations, but the available skills list is empty ### Root Cause: The function only understands source paths as **parent containers of skill directories**. It has no logic to detect that the source path itself **is** a skill directory (i.e., that `SKILL.md` might live directly at `skills/domainA/SKILL.md` when the source is `skills/domainA/`). The comment at line 151 confirms this is unintended behavior — passing a specific skill directory like `/skills/user/` is documented as a supported pattern. ### Proposed Fix: After listing the contents, check if a `SKILL.md` exists directly at `normalizedPath` itself. If so, treat the source as a single skill directory and parse its metadata directly. ### Can I work on this? I'd like to submit a PR to fix this issue. The approach would be: 1. Before the directory iteration loop, check if `SKILL.md` exists at the source path itself 2. If it does, parse it as a single skill and add to `loadedSkills` 3. Otherwise, continue with the existing subdirectory discovery logic Let me know if anyone else is working on this or if there's a preferred approach. Happy to collaborate!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#252