Subagents are using the task tool #2929

Open
opened 2026-02-16 17:37:52 -05:00 by yindo · 6 comments
Owner

Originally created by @arsham on GitHub (Nov 17, 2025).

Originally assigned to: @thdxr on GitHub.

Description

Although the subagents do not have the task tool but they sometimes delegate to other agents. I have noticed this happening after v1.0.68, but I can't tell which version it was.

Unfortunately the sub-agent got stuck somewhere, and I can't navigate to the sub-sub-agents either to see what is happening.

OpenCode version

v1.0.72

Steps to reproduce

Unfortunately I can't reliably produce a step for this. This is quite random. In the same session.

Screenshot and/or share link

Image

Shared session:

Operating System

Arch Linux

Terminal

foot

Originally created by @arsham on GitHub (Nov 17, 2025). Originally assigned to: @thdxr on GitHub. ### Description Although the subagents do not have the task tool but they sometimes delegate to other agents. I have noticed this happening after v1.0.68, but I can't tell which version it was. Unfortunately the sub-agent got stuck somewhere, and I can't navigate to the sub-sub-agents either to see what is happening. ### OpenCode version v1.0.72 ### Steps to reproduce Unfortunately I can't reliably produce a step for this. This is quite random. In the same session. ### Screenshot and/or share link <img width="3110" height="744" alt="Image" src="https://github.com/user-attachments/assets/fbcdfc29-8e62-40bb-9201-8459548498f2" /> Shared session: - Primary agent: https://opencode.ai/s/1LiETc7T - Sub-agent: https://opencode.ai/s/Lyh1uNbY ### Operating System Arch Linux ### Terminal foot
yindo added the bug label 2026-02-16 17:37:52 -05:00
Author
Owner

@arsham commented on GitHub (Nov 17, 2025):

Interesting find: https://opencode.ai/s/j0eMtkw1

This shows me some instructions that are only accessible by the primary agents

Image

I can confirm my subagent is set to "mode": "subagent". Here is a part of my config:

    "agent": {
        "build": {
            "mode": "primary",
            "color": "#c864af",
            "prompt": "{file:~/.config/opencode/config/primary/primary-role.md}{file:./config/primary/non-robin-constraints.md}",
        },

        "research:smart": {
            "mode": "subagent",
            "description": "Deep research specialist [preferred:research,reasoning,readonly] | Capabilities: Read-only file access | Smart model (sonnet) | Extended reasoning | Deep analysis | Selection criteria: Task requires understanding complex code, architecture, or patterns without modification | Use cases: Codebase exploration | Architectural review | Dependency analysis | Pattern identification | Impact assessment | Design investigation | Anti-patterns: Code implementation | File modifications | Quick lookups | Straightforward fact-finding",
            "prompt": "{file:./config/subagent/_directives.md}",
            "model": "anthropic/claude-sonnet-4-5-medium",
       }
    }
@arsham commented on GitHub (Nov 17, 2025): Interesting find: https://opencode.ai/s/j0eMtkw1 This shows me some instructions that are only accessible by the primary agents <img width="2460" height="1038" alt="Image" src="https://github.com/user-attachments/assets/208cf5a3-79e6-4087-8f9c-9966fab91743" /> I can confirm my subagent is set to `"mode": "subagent"`. Here is a part of my config: ```jsonc "agent": { "build": { "mode": "primary", "color": "#c864af", "prompt": "{file:~/.config/opencode/config/primary/primary-role.md}{file:./config/primary/non-robin-constraints.md}", }, "research:smart": { "mode": "subagent", "description": "Deep research specialist [preferred:research,reasoning,readonly] | Capabilities: Read-only file access | Smart model (sonnet) | Extended reasoning | Deep analysis | Selection criteria: Task requires understanding complex code, architecture, or patterns without modification | Use cases: Codebase exploration | Architectural review | Dependency analysis | Pattern identification | Impact assessment | Design investigation | Anti-patterns: Code implementation | File modifications | Quick lookups | Straightforward fact-finding", "prompt": "{file:./config/subagent/_directives.md}", "model": "anthropic/claude-sonnet-4-5-medium", } } ```
Author
Owner

@rekram1-node commented on GitHub (Nov 18, 2025):

ill check it out

@rekram1-node commented on GitHub (Nov 18, 2025): ill check it out
Author
Owner

@thdxr commented on GitHub (Nov 19, 2025):

the issue here is in the new agent loop it tries to resolve the tools from the last user message
but it's possible to insert a user message without any tools, need to think about how to resolve this/if this is even a good pattern i introduced

@thdxr commented on GitHub (Nov 19, 2025): the issue here is in the new agent loop it tries to resolve the tools from the last user message but it's possible to insert a user message without any tools, need to think about how to resolve this/if this is even a good pattern i introduced
Author
Owner

@arsham commented on GitHub (Nov 20, 2025):

I asked Claude to help investigate this and here's what it found. I'm not sure if this holds any truth to it, but I'm hoping this helps pinpointing the problem.

What Claude Found

The issue seems to trace back to the agent loop refactor in commit a1214fff. @thdxr you're right about it being tool resolution from user messages, but there's a specific execution path that causes it.

The Problem

File: packages/opencode/src/session/prompt.ts

The refactor extracted loop() as a separately callable function (line 229). The server can now call it directly from packages/opencode/src/server/server.ts:801:

await SessionPrompt.loop(id)

This bypasses the tool restrictions that were set up in the original prompt() call.

How It Happens

  1. Primary agent delegates via TaskTool with restricted tools: {task: false, todoread: false, todowrite: false}
  2. SessionPrompt.prompt() creates the user message with these restrictions
  3. loop(sessionID) gets called-works fine initially
  4. Later, the server calls SessionPrompt.loop(sessionID) directly for session continuation
  5. loop() tries to resolve tools from lastUser.tools (line 465), but if that message doesn't have complete tool restrictions, it falls back to agent defaults
  6. Subagent ends up with task tool access

Before vs After

Before refactor: The loop was internal to prompt(), so the tool context was consistent.

After refactor: Loop is exported and can be called independently, which breaks the guarantee that tool restrictions stick around.

Code Locations

Tool resolution in the loop (prompt.ts:461-467):

const tools = await resolveTools({
  agent,
  sessionID,
  model: lastUser.model,
  tools: lastUser.tools,  // Can be undefined/incomplete
  processor,
})

This would explain why it's random-it depends on the message history state when the server decides to continue the session.

Interesting Finding

With current implementation one issue we have is that when the sub-agent delegates, there is no way of navigating to the sub-sub-agent view/session to see what it is doing. However with the following change it is actually possible to navigate. It doesn't fix the issue in this conversation, but I thought it would be interesting to know:

M packages/opencode/src/server/server.ts
@@ -798,7 +798,7 @@ export namespace Server {
               modelID: body.modelID,
             },
           })
-          await SessionPrompt.loop(id)
+          await SessionPrompt.prompt({ sessionID: id, parts: [] })
           return c.json(true)
         },
       )
@arsham commented on GitHub (Nov 20, 2025): I asked Claude to help investigate this and here's what it found. I'm not sure if this holds any truth to it, but I'm hoping this helps pinpointing the problem. ## What Claude Found The issue seems to trace back to the agent loop refactor in commit a1214fff. @thdxr you're right about it being tool resolution from user messages, but there's a specific execution path that causes it. ### The Problem **File:** `packages/opencode/src/session/prompt.ts` The refactor extracted `loop()` as a separately callable function (line 229). The server can now call it directly from `packages/opencode/src/server/server.ts:801`: ```typescript await SessionPrompt.loop(id) ``` This bypasses the tool restrictions that were set up in the original `prompt()` call. ### How It Happens 1. Primary agent delegates via TaskTool with restricted tools: `{task: false, todoread: false, todowrite: false}` 2. `SessionPrompt.prompt()` creates the user message with these restrictions 3. `loop(sessionID)` gets called-works fine initially 4. Later, the server calls `SessionPrompt.loop(sessionID)` directly for session continuation 5. `loop()` tries to resolve tools from `lastUser.tools` (line 465), but if that message doesn't have complete tool restrictions, it falls back to agent defaults 6. Subagent ends up with task tool access ### Before vs After **Before refactor:** The loop was internal to `prompt()`, so the tool context was consistent. **After refactor:** Loop is exported and can be called independently, which breaks the guarantee that tool restrictions stick around. ### Code Locations Tool resolution in the loop (`prompt.ts:461-467`): ```typescript const tools = await resolveTools({ agent, sessionID, model: lastUser.model, tools: lastUser.tools, // Can be undefined/incomplete processor, }) ``` This would explain why it's random-it depends on the message history state when the server decides to continue the session. ### Interesting Finding With current implementation one issue we have is that when the sub-agent delegates, there is no way of navigating to the sub-sub-agent view/session to see what it is doing. However with the following change it is actually possible to navigate. It doesn't fix the issue in this conversation, but I thought it would be interesting to know: ```patch M packages/opencode/src/server/server.ts @@ -798,7 +798,7 @@ export namespace Server { modelID: body.modelID, }, }) - await SessionPrompt.loop(id) + await SessionPrompt.prompt({ sessionID: id, parts: [] }) return c.json(true) }, ) ```
Author
Owner

@arsham commented on GitHub (Nov 28, 2025):

@rekram1-node I did a bit of digging and I found some interesting lines in the code, some defaults might be the issue. I've applied this patch locally and I haven't seen the issue yet.

TLDR; there is something weird going on, but the default subagent can't be the build agent, it should have been the general agent.

diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 0936f530b..9f5005352 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -39,6 +39,7 @@ export namespace Session {
       id: Identifier.schema("session"),
       projectID: z.string(),
       directory: z.string(),
+      agent: z.string(),
       parentID: Identifier.schema("session").optional(),
       summary: z
         .object({
@@ -124,6 +125,7 @@ export namespace Session {
       .object({
         parentID: Identifier.schema("session").optional(),
         title: z.string().optional(),
+        agent: z.string().optional(),
       })
       .optional(),
     async (input) => {
@@ -131,6 +133,7 @@ export namespace Session {
         parentID: input?.parentID,
         directory: Instance.directory,
         title: input?.title,
+        agent: input?.agent,
       })
     },
   )
@@ -172,12 +175,19 @@ export namespace Session {
     })
   })
 
-  export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
+  export async function createNext(input: {
+    id?: string
+    title?: string
+    parentID?: string
+    directory: string
+    agent?: string
+  }) {
     const result: Info = {
       id: Identifier.descending("session", input.id),
       version: Installation.VERSION,
       projectID: Instance.project.id,
       directory: input.directory,
+      agent: input.agent ?? "build",
       parentID: input.parentID,
       title: input.title ?? createDefaultTitle(!!input.parentID),
       time: {
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 9611b5289..c32b94811 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -827,7 +827,10 @@ export namespace SessionPrompt {
   }
 
   async function createUserMessage(input: PromptInput) {
-    const agent = await Agent.get(input.agent ?? "build")
+    const session = await Session.get(input.sessionID)
+    const agent =
+      (await Agent.get(input.agent ?? "")) || (await Agent.get(session.agent ?? "")) || (await Agent.get("general"))
+    if (!agent) throw new Error(`Agent not found: ${input.agent ?? session.agent ?? "general"}`)
     const info: MessageV2.Info = {
       id: input.messageID ?? Identifier.ascending("message"),
       role: "user",
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index c4444f1b6..cd75ae2fc 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -61,6 +61,7 @@ export const TaskTool = Tool.define("task", async () => {
         return await Session.create({
           parentID: ctx.sessionID,
           title: params.description + ` (@${agent.name} subagent)`,
+          agent: agent.name,
         })
       })
       const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })

I am not happy about this though, this looks like a band-aid to me. A proper fix would be figuring out why sometimes the agent is not set properly.

@arsham commented on GitHub (Nov 28, 2025): @rekram1-node I did a bit of digging and I found some interesting lines in the code, some defaults might be the issue. I've applied this patch locally and I haven't seen the issue yet. **TLDR;** there is something weird going on, but the default subagent can't be the `build` agent, it should have been the `general` agent. ```patch diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0936f530b..9f5005352 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -39,6 +39,7 @@ export namespace Session { id: Identifier.schema("session"), projectID: z.string(), directory: z.string(), + agent: z.string(), parentID: Identifier.schema("session").optional(), summary: z .object({ @@ -124,6 +125,7 @@ export namespace Session { .object({ parentID: Identifier.schema("session").optional(), title: z.string().optional(), + agent: z.string().optional(), }) .optional(), async (input) => { @@ -131,6 +133,7 @@ export namespace Session { parentID: input?.parentID, directory: Instance.directory, title: input?.title, + agent: input?.agent, }) }, ) @@ -172,12 +175,19 @@ export namespace Session { }) }) - export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) { + export async function createNext(input: { + id?: string + title?: string + parentID?: string + directory: string + agent?: string + }) { const result: Info = { id: Identifier.descending("session", input.id), version: Installation.VERSION, projectID: Instance.project.id, directory: input.directory, + agent: input.agent ?? "build", parentID: input.parentID, title: input.title ?? createDefaultTitle(!!input.parentID), time: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9611b5289..c32b94811 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -827,7 +827,10 @@ export namespace SessionPrompt { } async function createUserMessage(input: PromptInput) { - const agent = await Agent.get(input.agent ?? "build") + const session = await Session.get(input.sessionID) + const agent = + (await Agent.get(input.agent ?? "")) || (await Agent.get(session.agent ?? "")) || (await Agent.get("general")) + if (!agent) throw new Error(`Agent not found: ${input.agent ?? session.agent ?? "general"}`) const info: MessageV2.Info = { id: input.messageID ?? Identifier.ascending("message"), role: "user", diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index c4444f1b6..cd75ae2fc 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -61,6 +61,7 @@ export const TaskTool = Tool.define("task", async () => { return await Session.create({ parentID: ctx.sessionID, title: params.description + ` (@${agent.name} subagent)`, + agent: agent.name, }) }) const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }) ``` I am not happy about this though, this looks like a band-aid to me. A proper fix would be figuring out why sometimes the agent is not set properly.
Author
Owner

@rekram1-node commented on GitHub (Nov 28, 2025):

ill take a looksie

@rekram1-node commented on GitHub (Nov 28, 2025): ill take a looksie
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#2929