Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/opencode/src/session/prompt/plan-mode.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ Goal: Gain a comprehensive understanding of the user's request by reading throug
### Phase 2: Design
Goal: Design an implementation approach.

Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
Launch explore agent(s) to check the design against the code, based on the user's intent and your exploration results from Phase 1. Only use the explore subagent type: plan mode denies subagents that can edit files.

You can launch up to 1 agent(s) in parallel.

**Guidelines:**
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
- **Default**: Launch at least 1 explore agent for most tasks - it helps validate your understanding and consider alternatives
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)

Examples of when to use multiple agents:
Expand Down
74 changes: 74 additions & 0 deletions packages/opencode/test/agent/plan-prompt-subagents.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { testEffect } from "../lib/effect"
import { Agent } from "../../src/agent/agent"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Wildcard } from "@opencode-ai/core/util/wildcard"
import { ErrorDetail } from "../../src/altimate/telemetry/error-detail"

const it = testEffect(
Agent.layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(LocationServiceMap.layer),
Layer.provide(RuntimeFlags.layer({})),
),
)

// The reported task denial (telemetry error_hash f55c9b41d367641d) is the plan agent's own
// task rule rejecting the general subagent, built and formatted as Permission.ask and the
// session processor do it.
it.instance("plan agent denying the general subagent reproduces the reported task error", () =>
Effect.gen(function* () {
const plan = (yield* Agent.Service.use((svc) => svc.list())).find((a) => a.name === "plan")!
expect(Permission.evaluate("task", "general", plan.permission).action).toBe("deny")
const denied = new PermissionV1.DeniedError({
ruleset: plan.permission.filter((rule) => Wildcard.match("task", rule.permission)),
})
// The tool error reaches the processor as a plain Error, so its text starts with "Error: ".
const detail = ErrorDetail.describeToolError(new Error(denied.message).toString())
expect(detail.errorMessage).toBe(
"error the user has specified a rule which <x> you from <x> this <x> tool call <x> are some of the <x> permission <x> permission task <x>",
)
expect(detail.errorHash).toBe("f55c9b41d367641d")
}),
)

// The plan agent denies some task subagents (general can edit). A plan prompt that tells the
// model to launch one of them makes every such task call fail with a permission denial.
for (const file of ["plan.txt", "plan-mode.txt"]) {
it.instance(`${file} only directs the plan agent to subagents it may launch`, () =>
Effect.gen(function* () {
const text = yield* Effect.promise(() =>
fs.readFile(path.join(__dirname, "../../src/session/prompt", file), "utf-8"),
)
const agents = yield* Agent.Service.use((svc) => svc.list())
const plan = agents.find((a) => a.name === "plan")
expect(plan).toBeDefined()
const named = agents.filter((a) =>
new RegExp(`\\b${a.name}\\b\`?\\s+(sub)?agent`, "i").test(text),
)
expect(named.map((a) => a.name)).toContain("explore")
for (const agent of named) {
expect({ agent: agent.name, mode: agent.mode }).toEqual({ agent: agent.name, mode: "subagent" })
expect({ agent: agent.name, action: Permission.evaluate("task", agent.name, plan!.permission).action }).toEqual({
agent: agent.name,
action: "allow",
})
}
}),
)
}
Loading