diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 171c4b448fd..59a3e867793 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1267,15 +1267,24 @@ export namespace SessionPrompt { } if (part.type === "agent") { + const agentPart = { + ...part, + messageID: info.id, + sessionID: input.sessionID, + } + // Only inject the delegation nudge in root sessions. Subagent sessions + // already have an explicit task prompt; injecting it there causes + // recursive nesting bias when the agent sees @agent mentions in its + // own context. + const currentSession = await Session.get(input.sessionID) + if (currentSession.parentID) { + return [agentPart] + } // Check if this agent would be denied by task permission const perm = PermissionNext.evaluate("task", part.name, agent.permission) const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : "" return [ - { - ...part, - messageID: info.id, - sessionID: input.sessionID, - }, + agentPart, { messageID: info.id, sessionID: input.sessionID, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 3986271dab9..b36cfaea8db 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -148,6 +148,87 @@ describe("session.prompt special characters", () => { }) }) + +describe("session.prompt agent part", () => { + const delegationNudge = + "Use the above message and context to generate a prompt and call the task tool with subagent:"; + + test("injects synthetic delegation nudge in root sessions", async () => { + await using tmp = await tmpdir({ git: true }); + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create({}); + + const msg = await SessionPrompt.prompt({ + sessionID: session.id, + noReply: true, + parts: [ + { type: "text", text: "do something" }, + { type: "agent", name: "build" }, + ], + }); + + if (msg.info.role !== "user") throw new Error("expected user message"); + + const stored = await MessageV2.get({ + sessionID: session.id, + messageID: msg.info.id, + }); + const syntheticDelegation = stored.parts.find( + (part) => + part.type === "text" && part.synthetic && + part.text.includes(delegationNudge), + ); + expect(syntheticDelegation).toBeDefined(); + + await Session.remove(session.id); + }, + }); + }); + + test("skips synthetic delegation nudge in subagent sessions", async () => { + await using tmp = await tmpdir({ git: true }); + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const root = await Session.create({}); + const child = await Session.create({ parentID: root.id }); + + const msg = await SessionPrompt.prompt({ + sessionID: child.id, + noReply: true, + parts: [ + { type: "text", text: "do something" }, + { type: "agent", name: "build" }, + ], + }); + + if (msg.info.role !== "user") throw new Error("expected user message"); + + const stored = await MessageV2.get({ + sessionID: child.id, + messageID: msg.info.id, + }); + const syntheticDelegation = stored.parts.find( + (part) => + part.type === "text" && part.synthetic && + part.text.includes(delegationNudge), + ); + expect(syntheticDelegation).toBeUndefined(); + + const agentPart = stored.parts.find((part) => part.type === "agent"); + expect(agentPart).toBeDefined(); + + await Session.remove(child.id); + await Session.remove(root.id); + }, + }); + }); +}); + describe("session.prompt agent variant", () => { test("applies agent variant only when using agent model", async () => { const prev = process.env.OPENAI_API_KEY