Skip to content

Load subagent names from agents/ markdown files - #117

Merged
Nomadcxx merged 9 commits into
Nomadcxx:mainfrom
OZoneGuy:feat/read-agents-from-directory
Jul 24, 2026
Merged

Load subagent names from agents/ markdown files#117
Nomadcxx merged 9 commits into
Nomadcxx:mainfrom
OZoneGuy:feat/read-agents-from-directory

Conversation

@OZoneGuy

@OZoneGuy OZoneGuy commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Extend readSubagentNames to load agents from agents/*.md next to the OpenCode config, in addition to the agent section in opencode.json
  • Parse markdown frontmatter for mode and disable, merge with JSON-defined agents (directory wins on name clash), and keep existing subagent selection behavior
  • Add unit tests for directory-only agents, merge/override, and disabled markdown agents

Test plan

  • bun test tests/unit/mcp-config.test.ts

Made with Cursor


I have my agents defined in markdown files for opencode, https://opencode.ai/docs/agents/#markdown, and they are not picked up by the plugin. Adding code to pick up the markdown agents.

OpenCode also defines agents as *.md files under the config agents directory, so readSubagentNames now merges those definitions with opencode.json agents.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Nomadcxx

Copy link
Copy Markdown
Owner

FYI, three follow-ups that align this with how OpenCode itself loads agents (packages/opencode/src/config/agent.ts). I will push them on top of your branch so this PR carries them.

  1. Read agent/ as well as agents/. OpenCode globs {agent,agents}/**/*.md, so anyone on the singular agent/ layout gets nothing from the current code. This is the main gap.

  2. Recurse into subdirectories. The flat readdirSync skips nested agent files, and the basename-only name can differ from the path-based name OpenCode uses.

  3. Handle quoted and alternate frontmatter values. mode: "subagent" keeps its quotes under the current regex and stops matching subagent, and disable: "true" or disable: yes slips past the exact === "true" check. Strip the quotes and accept the truthy forms. No YAML dependency, so your regex approach stays.

Commits to follow. Thanks for kicking this off.

Read from both `agent/` and `agents/` (OpenCode globs {agent,agents}/**/*.md),
scan recursively with path-based names, and parse quoted/alternate frontmatter
values (`mode: "subagent"`, `disable: "true"`, `disable: yes`).

Adds unit tests plus a real-filesystem test exercising both directories and
nested agents on disk.
@Nomadcxx

Copy link
Copy Markdown
Owner

Pushed the three fixes as a commit on top. Covered by unit tests and a real-filesystem test that writes agent/ and agents/ trees to disk (with a nested agent) and reads them back, so the recursive readdirSync and path-based naming run for real, not just against mocks.

Still needs a live check before merge: confirm that in a running OpenCode session the discovered names actually reach the task tool, so the model can spawn an agent defined only in agent/foo.md. The loader is verified end to end at the filesystem layer; the path through OpenCode subagent selection is not, since that needs a real session.

@Nomadcxx

Copy link
Copy Markdown
Owner

Update: the live OpenCode check changed the implementation direction, so I have pushed five commits on top of Omar's branch. I kept the original disable fix as the base.

Why the implementation changed

OpenCode resolves agent/ and agents/, applies config precedence, and puts the merged agent list in the runtime Task tool description. Scanning those files inside the plugin duplicated OpenCode's loader and gave us another implementation that could drift from it.

The plugin uses the Task description OpenCode sends with each request. I removed readSubagentNames(), its filesystem cache, and subagentFingerprint. The normal tool fingerprint hashes the full Task description, so an agent-list change invalidates a resumed Cursor session.

Task bridge

Cursor Agent cannot call OpenCode's Task tool through its native tool enum. The patch gives it one strict JSON envelope:

{"name":"task","arguments":{"description":"Run proof","prompt":"Do the work","subagent_type":"project-proof"}}

The plugin converts that envelope into an OpenAI task call only when OpenCode offered the tool. It validates the three required strings and the optional task_id and command fields before dispatch.

The stream detector covers split deltas, cumulative snapshots, fences, malformed JSON, native tool boundaries, and stream-end flushes. Both Bun and Node use the same detector. A valid envelope produces one Task call and does not leak into assistant text. Mixed thinking and text events keep their reasoning while the detector buffers the JSON.

Write behaviour

The detector treats ordinary prose as the start of an assistant response. A later Write envelope remains text until a native tool event starts a new phase. This removes the old last-line extraction heuristic. A JSON candidate followed by prose stays buffered until the next boundary or stream end, then returns verbatim.

Verification

  • npm run test:ci:unit: 564 passed, 0 failed
  • npm run test:ci:integration: 43 passed, 1 existing skip, 0 failed
  • npm run build: passed
  • go test ./...: passed
  • Clean merge and the same checks against current origin/main: 570 unit tests and 43 integration tests passed

I also ran a live isolated session with cursor-acp/auto and a project-local project-proof subagent. OpenCode offered that agent through the Task description, Cursor emitted the bridge envelope, the plugin dispatched one Task call, and the child and parent both returned PR117_SUBAGENT_OK. The run had no raw envelope text or native Task enum error.

@OZoneGuy, the main review point is the change from filesystem discovery to OpenCode-owned runtime metadata. It removes most of the original loader code while preserving the feature the PR set out to add. Thanks for starting this.

@Nomadcxx
Nomadcxx merged commit 494c6e3 into Nomadcxx:main Jul 24, 2026
2 checks passed
@OZoneGuy

Copy link
Copy Markdown
Contributor Author

Thanks for following up on this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants