[FEATURE]: Lazy-load agent/skill lists instead of inlining into tool descriptions #9102

Open
opened 2026-02-16 18:11:39 -05:00 by yindo · 1 comment
Owner

Originally created by @Tefx on GitHub (Feb 11, 2026).

Originally assigned to: @thdxr on GitHub.

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem

The task and skill tool descriptions are statically inlined with the full list of all available agents/skills on every API call. This means every turn pays a fixed token cost proportional to the number of configured agents and skills, regardless of whether the user needs them.

How it works today

task.ts — on every init(), fetches all non-primary agents and concatenates their names + descriptions into the tool's description field:

const agents = await Agent.list().then(x => x.filter(a => a.mode !== "primary"))
const description = DESCRIPTION.replace("{agents}",
  accessibleAgents.map(a => `- ${a.name}: ${a.description}`).join("\n")
)

skill.ts — similarly inlines all skills as XML blocks into the tool description:

...accessibleSkills.flatMap((skill) => [
  `  <skill>`,
  `    <name>${skill.name}</name>`,
  `    <description>${skill.description}</description>`,
  `    <location>${pathToFileURL(skill.location).href}</location>`,
  `  </skill>`,
])

Token cost

With 21 custom agents and 3 skills, this adds ~2,000 tokens per turn just in tool descriptions. For power users with 27+ agents and 7+ skills (common in multi-project setups), this can reach ~3,500 tokens per turn — entirely wasted when the user doesn't need to launch a subagent or load a skill in that particular turn.

Combined with MCP tools (see #7399) and other built-in tool descriptions (see #9858), the fixed overhead can consume 15-20% of the context window before any conversation even starts.

Proposed Solution

Make agent/skill lists dynamically loaded on demand instead of statically inlined.

Option A: Two-step tool call (recommended)

The task tool description becomes a fixed ~200 tokens:

Launch a new agent to handle complex, multistep tasks autonomously.
Call with action="list" to see available agent types, or provide a
subagent_type to launch an agent directly.
[usage notes...]

When the LLM needs to pick an agent, it calls task(action="list") → returns the full agent catalog as a tool result. This result lives in conversation history and is naturally available for subsequent turns without re-sending.

Same pattern for skill:

Load a specialized skill. Call with action="list" to see available
skills, or provide a skill name to load it.

Option B: Summary in description, detail on demand

Keep a one-line-per-agent summary in the description (just names, no descriptions), and add a task(action="describe", agent="foo") to get the full description when needed.

Benefits

  • Saves ~2-3.5k tokens per turn for users with many agents/skills
  • Scales gracefully — 100 agents costs the same as 3 until you actually need the list
  • Consistent with how MCP tools themselves work (tools are registered, not described inline)
  • The agent/skill list is typically only needed once per conversation (first time a subagent is spawned)

Related issues

  • #7399 — MCP tool filtering (complementary, addresses a different axis of tool bloat)
  • #9858 — Token consumption analysis (documents the problem, this issue proposes a solution)
Originally created by @Tefx on GitHub (Feb 11, 2026). Originally assigned to: @thdxr on GitHub. ### Feature hasn't been suggested before. - [x] I have verified this feature I'm about to request hasn't been suggested before. ### Describe the enhancement you want to request ## Problem The `task` and `skill` tool descriptions are statically inlined with the **full list** of all available agents/skills on every API call. This means every turn pays a fixed token cost proportional to the number of configured agents and skills, regardless of whether the user needs them. ### How it works today **`task.ts`** — on every `init()`, fetches all non-primary agents and concatenates their names + descriptions into the tool's `description` field: ```ts const agents = await Agent.list().then(x => x.filter(a => a.mode !== "primary")) const description = DESCRIPTION.replace("{agents}", accessibleAgents.map(a => `- ${a.name}: ${a.description}`).join("\n") ) ``` **`skill.ts`** — similarly inlines all skills as XML blocks into the tool description: ```ts ...accessibleSkills.flatMap((skill) => [ ` <skill>`, ` <name>${skill.name}</name>`, ` <description>${skill.description}</description>`, ` <location>${pathToFileURL(skill.location).href}</location>`, ` </skill>`, ]) ``` ### Token cost With 21 custom agents and 3 skills, this adds **~2,000 tokens per turn** just in tool descriptions. For power users with 27+ agents and 7+ skills (common in multi-project setups), this can reach **~3,500 tokens per turn** — entirely wasted when the user doesn't need to launch a subagent or load a skill in that particular turn. Combined with MCP tools (see #7399) and other built-in tool descriptions (see #9858), the fixed overhead can consume 15-20% of the context window before any conversation even starts. ## Proposed Solution Make agent/skill lists **dynamically loaded on demand** instead of statically inlined. ### Option A: Two-step tool call (recommended) The `task` tool description becomes a fixed ~200 tokens: ``` Launch a new agent to handle complex, multistep tasks autonomously. Call with action="list" to see available agent types, or provide a subagent_type to launch an agent directly. [usage notes...] ``` When the LLM needs to pick an agent, it calls `task(action="list")` → returns the full agent catalog as a tool result. This result lives in conversation history and is naturally available for subsequent turns without re-sending. Same pattern for `skill`: ``` Load a specialized skill. Call with action="list" to see available skills, or provide a skill name to load it. ``` ### Option B: Summary in description, detail on demand Keep a one-line-per-agent summary in the description (just names, no descriptions), and add a `task(action="describe", agent="foo")` to get the full description when needed. ### Benefits - **Saves ~2-3.5k tokens per turn** for users with many agents/skills - Scales gracefully — 100 agents costs the same as 3 until you actually need the list - Consistent with how MCP tools themselves work (tools are registered, not described inline) - The agent/skill list is typically only needed once per conversation (first time a subagent is spawned) ### Related issues - #7399 — MCP tool filtering (complementary, addresses a different axis of tool bloat) - #9858 — Token consumption analysis (documents the problem, this issue proposes a solution)
Author
Owner

@github-actions[bot] commented on GitHub (Feb 11, 2026):

Thanks for the detailed feature request! This complements #8277 (lazy loading for MCP tools) nicely — while #8277 tackles context bloat from external MCP servers, this issue addresses the same problem for internal agents and skills.

Both follow similar patterns and could potentially share implementation approaches. Worth keeping an eye on the discussion in #8277 for any relevant insights.

@github-actions[bot] commented on GitHub (Feb 11, 2026): Thanks for the detailed feature request! This complements #8277 (lazy loading for MCP tools) nicely — while #8277 tackles context bloat from external MCP servers, this issue addresses the same problem for internal agents and skills. Both follow similar patterns and could potentially share implementation approaches. Worth keeping an eye on the discussion in #8277 for any relevant insights.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#9102