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

@ -44,11 +44,13 @@ export class Info extends Schema.Class<Info>("AgentV2.Info")({
type Data = {
agents: Map<ID, Info>
default?: ID
}
export type Editor = {
list: () => readonly Info[]
get: (id: ID) => Info | undefined
default: (id: ID | undefined) => void
update: (id: ID, fn: (agent: Draft<Info>) => void) => void
remove: (id: ID) => void
}
@ -57,6 +59,8 @@ export interface Interface {
readonly transform: State.Interface<Data, Editor>["transform"]
readonly update: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
readonly get: (id: ID) => Effect.Effect<Info | undefined>
readonly default: () => Effect.Effect<Info | undefined>
readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
readonly all: () => Effect.Effect<Info[]>
}
@ -72,6 +76,9 @@ export const layer = Layer.effect(
editor: (draft) => ({
list: () => Array.fromIterable(draft.agents.values()) as Info[],
get: (id) => draft.agents.get(id),
default: (id) => {
draft.default = id
},
update: (id, fn) => {
const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
if (!draft.agents.has(id)) draft.agents.set(id, current)
@ -83,6 +90,19 @@ export const layer = Layer.effect(
},
}),
})
const selectable = (agent: Info | undefined) =>
agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined
const selectedDefault = () => {
const data = state.get()
const configured = data.default ? selectable(data.agents.get(data.default)) : undefined
if (configured) return configured
const build = selectable(data.agents.get(ID.make("build")))
if (build) return build
for (const agent of data.agents.values()) {
const fallback = selectable(agent)
if (fallback) return fallback
}
}
return Service.of({
transform: state.transform,
@ -93,6 +113,13 @@ export const layer = Layer.effect(
get: Effect.fn("AgentV2.get")(function* (id) {
return state.get().agents.get(id)
}),
default: Effect.fn("AgentV2.default")(function* () {
return selectedDefault()
}),
resolve: Effect.fn("AgentV2.resolve")(function* (id) {
if (id !== undefined) return state.get().agents.get(ID.make(id))
return selectedDefault()
}),
all: Effect.fn("AgentV2.all")(function* () {
return Array.fromIterable(state.get().agents.values())
}),

View file

@ -35,6 +35,9 @@ export class Info extends Schema.Class<Info>("Config.Info")({
model: Schema.String.pipe(Schema.optional).annotate({
description: "Default model to use when no session or agent model is selected",
}),
default_agent: Schema.String.pipe(Schema.optional).annotate({
description: "Default primary agent to use when no session agent is selected",
}),
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")])
.pipe(Schema.optional)
.annotate({
@ -130,6 +133,9 @@ export const layer = Layer.effect(
const location = yield* Location.Service
const policy = yield* Policy.Service
const names = ["config.json", "opencode.json", "opencode.jsonc"]
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions)
const loadFile = Effect.fnUntraced(function* (filepath: string) {
const text = yield* fs.readFileStringSafe(filepath)
@ -139,21 +145,10 @@ export const layer = Layer.effect(
const input: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length) return
const decoded = ConfigMigrateV1.isV1(input)
? Option.map(
Schema.decodeUnknownOption(ConfigV1.Info)(input, {
errors: "all",
onExcessProperty: "ignore",
propertyOrder: "original",
}),
ConfigMigrateV1.migrate,
)
: Option.some(input)
const info = Option.getOrUndefined(
Option.flatMap(
decoded,
Schema.decodeUnknownOption(Info, { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" }),
),
ConfigMigrateV1.isV1(input)
? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
: decodeInfo(input),
)
if (!info) return
return new Document({ type: "document", path: filepath, info })

View file

@ -58,6 +58,8 @@ export const Plugin = PluginV2.define({
yield* agent.update((editor) => {
const global = documents.flatMap((document) => document.info.permissions ?? [])
const configuredDefault = documents.findLast((document) => document.info.default_agent !== undefined)?.info.default_agent
if (configuredDefault !== undefined) editor.default(AgentV2.ID.make(configuredDefault))
for (const current of editor.list()) {
editor.update(current.id, (agent) => agent.permissions.push(...global))
}

View file

@ -16,6 +16,7 @@ export { Effect, Rule, Ruleset } from "./permission/schema"
type Effect = PermissionSchema.Effect
type Rule = PermissionSchema.Rule
type Ruleset = PermissionSchema.Ruleset
const missingAgentPermissions: Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
Schema.brand("PermissionV2.ID"),
@ -161,7 +162,7 @@ export const layer = Layer.effect(
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID) {
const session = yield* sessions.get(sessionID)
if (!session) return yield* new SessionV2.NotFoundError({ sessionID })
return (yield* agents.get(AgentV2.ID.make(session.agent ?? "build")))?.permissions ?? []
return (yield* agents.resolve(session.agent))?.permissions ?? missingAgentPermissions
})
function denied(input: AssertInput, rules: Ruleset) {

View file

@ -9,6 +9,8 @@ import { PermissionV2 } from "../permission"
import { PluginV2 } from "../plugin"
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
const BUILD_SYSTEM =
"You are an AI coding agent. Help the user accomplish software engineering tasks by inspecting the workspace, making targeted changes, and using tools according to the configured permissions."
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
@ -123,6 +125,7 @@ export const Plugin = PluginV2.define({
yield* agent.update((editor) => {
editor.update(AgentV2.ID.make("build"), (item) => {
item.description = "The default agent. Executes tools based on configured permissions."
item.system ??= BUILD_SYSTEM
item.mode = "primary"
item.permissions.push(
...PermissionV2.merge(defaults, [

View file

@ -16,6 +16,7 @@ import { SessionInput } from "../input"
import { QuestionV2 } from "../../question"
import { SystemContextRegistry } from "../../system-context-registry"
import { SessionContextEpoch } from "../context-epoch"
import { AgentV2 } from "../../agent"
/**
* Runs one durable coding-agent Session until it settles.
@ -84,6 +85,7 @@ export const layer = Layer.effect(
Effect.gen(function* () {
const events = yield* EventV2.Service
const llm = yield* LLMClient.Service
const agents = yield* AgentV2.Service
const tools = yield* ToolRegistry.Service
const models = yield* SessionRunnerModel.Service
const store = yield* SessionStore.Service
@ -134,6 +136,7 @@ export const layer = Layer.effect(
const session = yield* getSession(sessionID)
const initialized = yield* SessionContextEpoch.initialize(db, systemContext, session.id, session.location)
const model = yield* models.resolve(session)
const agent = yield* agents.resolve(session.agent)
const toolFibers = yield* FiberSet.make<void, never>()
let needsContinuation = false
if (promotion) {
@ -149,13 +152,13 @@ export const layer = Layer.effect(
const context = yield* store.runnerContext(session.id, system.baselineSeq)
const request = LLM.request({
model,
system: system.baseline.length > 0 ? [SystemPart.make(system.baseline)] : [],
system: [agent?.system, system.baseline].filter((part): part is string => part !== undefined && part.length > 0).map(SystemPart.make),
messages: toLLMMessages(context, model),
tools: yield* tools.definitions(),
})
const publisher = createLLMEventPublisher(events, {
sessionID: session.id,
agent: session.agent ?? "build",
agent: agent?.id ?? "build",
model: {
id: ModelV2.ID.make(model.id),
providerID: ProviderV2.ID.make(model.provider),

View file

@ -18,7 +18,6 @@ const keys = new Set([
"disabled_providers",
"enabled_providers",
"small_model",
"default_agent",
"mode",
"agent",
"provider",
@ -38,6 +37,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
$schema: info.$schema,
shell: info.shell,
model: info.model,
default_agent: info.default_agent,
autoupdate: info.autoupdate,
share: info.share ?? (info.autoshare ? "auto" : undefined),
enterprise: info.enterprise,