Skip to content

Subagents are using the task tool #4439

Closed

Description

@arsham

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

Activity

  1. added
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Nov 18, 2025
  2. arsham commented on Nov 18, 2025

    @arsham
    Author

    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",
           }
        }
  3. self-assigned this
    on Nov 18, 2025
  4. removed
    opentuiThis relates to changes in v1.0, now that opencode uses opentui
    on Nov 18, 2025
  5. rekram1-node commented on Nov 18, 2025

    @rekram1-node
    Contributor

    ill check it out

  6. thdxr commented on Nov 19, 2025

    @thdxr
    Member

    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

  7. arsham commented on Nov 20, 2025

    @arsham
    Author

    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 a1214ff. @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)
             },
           )
  8. arsham commented on Nov 28, 2025

    @arsham
    Author

    @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.

  9. rekram1-node commented on Nov 29, 2025

    @rekram1-node
    Contributor

    ill take a looksie

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions