fix(core): respect v2 default agents (#30969)

This commit is contained in:
Kit Langton 2026-06-05 10:47:31 -04:00 committed by GitHub
commit 02a5ae6585
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 155 additions and 18 deletions

View file

@ -244,6 +244,7 @@ describe("Config", () => {
JSON.stringify({
shell: "/bin/bash",
model: "anthropic/claude",
default_agent: "reviewer",
autoupdate: "notify",
share: "disabled",
enterprise: { url: "https://share.example.com" },
@ -328,6 +329,7 @@ describe("Config", () => {
expect(documents).toHaveLength(1)
expect(documents[0]?.info.shell).toBe("/bin/bash")
expect(documents[0]?.info.model).toBe("anthropic/claude")
expect(documents[0]?.info.default_agent).toBe("reviewer")
expect(documents[0]?.info.autoupdate).toBe("notify")
expect(documents[0]?.info.share).toBe("disabled")
expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" })
@ -427,6 +429,7 @@ describe("Config", () => {
path.join(tmp.path, "opencode.json"),
JSON.stringify({
shell: "/bin/zsh",
default_agent: "reviewer",
snapshot: false,
autoshare: true,
permission: {
@ -487,6 +490,7 @@ describe("Config", () => {
expect(documents).toHaveLength(1)
expect(documents[0]?.info).toBeInstanceOf(Config.Info)
expect(documents[0]?.info.shell).toBe("/bin/zsh")
expect(documents[0]?.info.default_agent).toBe("reviewer")
expect(documents[0]?.info.snapshots).toBe(false)
expect(documents[0]?.info.share).toBe("auto")
expect(documents[0]?.info.permissions).toEqual([

View file

@ -165,6 +165,28 @@ describe("PermissionV2", () => {
}),
)
it.effect("denies omitted-agent permissions when no primary default agent exists", () =>
Effect.gen(function* () {
yield* setup()
const { db } = yield* Database.Service
yield* db
.update(SessionTable)
.set({ agent: null })
.where(eq(SessionTable.id, SessionV2.ID.make("ses_test")))
.run()
.pipe(Effect.orDie)
const agents = yield* AgentV2.Service
yield* agents.update((editor) => {
editor.remove(AgentV2.ID.make("test"))
editor.remove(AgentV2.ID.make("build"))
})
const service = yield* PermissionV2.Service
expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "deny" })
expect(yield* service.list()).toEqual([])
}),
)
it.effect("evaluates bash with the normal configured-rule semantics", () =>
Effect.gen(function* () {
yield* setup([{ action: "*", resource: "*", effect: "allow" }])

View file

@ -6,6 +6,7 @@ import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event"
import { EventTable } from "@opencode-ai/core/event/sql"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -48,6 +49,7 @@ const permission = Layer.succeed(
}),
)
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
const agents = AgentV2.layer
const model = OpenAIChat.route
.with({
endpoint: { baseURL: "https://api.openai.com/v1" },
@ -65,6 +67,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
Layer.provide(registry),
Layer.provide(models),
Layer.provide(systemContext),
Layer.provide(agents),
)
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
const execution = Layer.effect(
@ -89,6 +92,7 @@ const it = testEffect(
executor,
client,
permission,
agents,
registry,
models,
systemContext,

View file

@ -32,6 +32,7 @@ import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { AgentV2 } from "@opencode-ai/core/agent"
import { NativeTool } from "@opencode-ai/core/tool/native"
import {
SessionContextEpochTable,
@ -107,6 +108,7 @@ const permission = Layer.succeed(
)
const applications = ApplicationTools.layer
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(applications))
const agents = AgentV2.layer
const echo = Layer.effectDiscard(
ToolRegistry.Service.use((registry) =>
registry.contribute((editor) => {
@ -189,6 +191,7 @@ const runner = SessionRunnerLLM.layer.pipe(
Layer.provide(registry),
Layer.provide(models),
Layer.provide(systemContext),
Layer.provide(agents),
)
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
const execution = Layer.effect(
@ -214,6 +217,7 @@ const it = testEffect(
client,
permission,
applications,
agents,
registry,
echo,
models,
@ -724,6 +728,78 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("includes the effective default agent system before durable context", () =>
Effect.gen(function* () {
yield* setup
const agent = yield* AgentV2.Service
yield* agent.update((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.system = "Build agent instructions"
agent.mode = "primary"
}),
)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-build", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"])
}),
)
it.effect("uses the configured default agent system for omitted-agent sessions", () =>
Effect.gen(function* () {
yield* setup
const agent = yield* AgentV2.Service
yield* agent.update((editor) => {
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.system = "Build agent instructions"
agent.mode = "primary"
})
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.system = "Reviewer instructions"
agent.mode = "primary"
})
editor.default(AgentV2.ID.make("reviewer"))
})
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-reviewer", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"])
expect((yield* session.messages({ sessionID }))[0]).toMatchObject({ type: "assistant", agent: "reviewer" })
}),
)
it.effect("uses an explicitly selected non-build agent system", () =>
Effect.gen(function* () {
yield* setup
const { db } = yield* Database.Service
const agent = yield* AgentV2.Service
yield* agent.update((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.system = "Reviewer instructions"
agent.mode = "primary"
}),
)
yield* db.update(SessionTable).set({ agent: "reviewer" }).where(eq(SessionTable.id, sessionID)).run().pipe(Effect.orDie)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false })
requests.length = 0
response = fragmentFixture("text", "text-selected", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"])
expect((yield* session.messages({ sessionID }))[0]).toMatchObject({ type: "assistant", agent: "reviewer" })
}),
)
it.effect("admits removed context as a chronological System message", () =>
Effect.gen(function* () {
yield* setup