fix(core): respect v2 default agents (#30969)
This commit is contained in:
parent
fff36b70bc
commit
02a5ae6585
11 changed files with 155 additions and 18 deletions
|
|
@ -44,11 +44,13 @@ export class Info extends Schema.Class<Info>("AgentV2.Info")({
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
agents: Map<ID, Info>
|
agents: Map<ID, Info>
|
||||||
|
default?: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Editor = {
|
export type Editor = {
|
||||||
list: () => readonly Info[]
|
list: () => readonly Info[]
|
||||||
get: (id: ID) => Info | undefined
|
get: (id: ID) => Info | undefined
|
||||||
|
default: (id: ID | undefined) => void
|
||||||
update: (id: ID, fn: (agent: Draft<Info>) => void) => void
|
update: (id: ID, fn: (agent: Draft<Info>) => void) => void
|
||||||
remove: (id: ID) => void
|
remove: (id: ID) => void
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +59,8 @@ export interface Interface {
|
||||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
readonly update: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
readonly update: (update: State.Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
|
||||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
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[]>
|
readonly all: () => Effect.Effect<Info[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +76,9 @@ export const layer = Layer.effect(
|
||||||
editor: (draft) => ({
|
editor: (draft) => ({
|
||||||
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
list: () => Array.fromIterable(draft.agents.values()) as Info[],
|
||||||
get: (id) => draft.agents.get(id),
|
get: (id) => draft.agents.get(id),
|
||||||
|
default: (id) => {
|
||||||
|
draft.default = id
|
||||||
|
},
|
||||||
update: (id, fn) => {
|
update: (id, fn) => {
|
||||||
const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
|
const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
|
||||||
if (!draft.agents.has(id)) draft.agents.set(id, current)
|
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({
|
return Service.of({
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
|
|
@ -93,6 +113,13 @@ export const layer = Layer.effect(
|
||||||
get: Effect.fn("AgentV2.get")(function* (id) {
|
get: Effect.fn("AgentV2.get")(function* (id) {
|
||||||
return state.get().agents.get(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* () {
|
all: Effect.fn("AgentV2.all")(function* () {
|
||||||
return Array.fromIterable(state.get().agents.values())
|
return Array.fromIterable(state.get().agents.values())
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
||||||
model: Schema.String.pipe(Schema.optional).annotate({
|
model: Schema.String.pipe(Schema.optional).annotate({
|
||||||
description: "Default model to use when no session or agent model is selected",
|
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")])
|
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")])
|
||||||
.pipe(Schema.optional)
|
.pipe(Schema.optional)
|
||||||
.annotate({
|
.annotate({
|
||||||
|
|
@ -130,6 +133,9 @@ export const layer = Layer.effect(
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const policy = yield* Policy.Service
|
const policy = yield* Policy.Service
|
||||||
const names = ["config.json", "opencode.json", "opencode.jsonc"]
|
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 loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||||
const text = yield* fs.readFileStringSafe(filepath)
|
const text = yield* fs.readFileStringSafe(filepath)
|
||||||
|
|
@ -139,21 +145,10 @@ export const layer = Layer.effect(
|
||||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||||
if (errors.length) return
|
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(
|
const info = Option.getOrUndefined(
|
||||||
Option.flatMap(
|
ConfigMigrateV1.isV1(input)
|
||||||
decoded,
|
? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
|
||||||
Schema.decodeUnknownOption(Info, { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" }),
|
: decodeInfo(input),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if (!info) return
|
if (!info) return
|
||||||
return new Document({ type: "document", path: filepath, info })
|
return new Document({ type: "document", path: filepath, info })
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ export const Plugin = PluginV2.define({
|
||||||
|
|
||||||
yield* agent.update((editor) => {
|
yield* agent.update((editor) => {
|
||||||
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
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()) {
|
for (const current of editor.list()) {
|
||||||
editor.update(current.id, (agent) => agent.permissions.push(...global))
|
editor.update(current.id, (agent) => agent.permissions.push(...global))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export { Effect, Rule, Ruleset } from "./permission/schema"
|
||||||
type Effect = PermissionSchema.Effect
|
type Effect = PermissionSchema.Effect
|
||||||
type Rule = PermissionSchema.Rule
|
type Rule = PermissionSchema.Rule
|
||||||
type Ruleset = PermissionSchema.Ruleset
|
type Ruleset = PermissionSchema.Ruleset
|
||||||
|
const missingAgentPermissions: Ruleset = [{ action: "*", resource: "*", effect: "deny" }]
|
||||||
|
|
||||||
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
|
export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe(
|
||||||
Schema.brand("PermissionV2.ID"),
|
Schema.brand("PermissionV2.ID"),
|
||||||
|
|
@ -161,7 +162,7 @@ export const layer = Layer.effect(
|
||||||
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID) {
|
const configured = EffectRuntime.fn("PermissionV2.configured")(function* (sessionID: SessionV2.ID) {
|
||||||
const session = yield* sessions.get(sessionID)
|
const session = yield* sessions.get(sessionID)
|
||||||
if (!session) return yield* new SessionV2.NotFoundError({ 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) {
|
function denied(input: AssertInput, rules: Ruleset) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { PermissionV2 } from "../permission"
|
||||||
import { PluginV2 } from "../plugin"
|
import { PluginV2 } from "../plugin"
|
||||||
|
|
||||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
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.
|
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) => {
|
yield* agent.update((editor) => {
|
||||||
editor.update(AgentV2.ID.make("build"), (item) => {
|
editor.update(AgentV2.ID.make("build"), (item) => {
|
||||||
item.description = "The default agent. Executes tools based on configured permissions."
|
item.description = "The default agent. Executes tools based on configured permissions."
|
||||||
|
item.system ??= BUILD_SYSTEM
|
||||||
item.mode = "primary"
|
item.mode = "primary"
|
||||||
item.permissions.push(
|
item.permissions.push(
|
||||||
...PermissionV2.merge(defaults, [
|
...PermissionV2.merge(defaults, [
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { SessionInput } from "../input"
|
||||||
import { QuestionV2 } from "../../question"
|
import { QuestionV2 } from "../../question"
|
||||||
import { SystemContextRegistry } from "../../system-context-registry"
|
import { SystemContextRegistry } from "../../system-context-registry"
|
||||||
import { SessionContextEpoch } from "../context-epoch"
|
import { SessionContextEpoch } from "../context-epoch"
|
||||||
|
import { AgentV2 } from "../../agent"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs one durable coding-agent Session until it settles.
|
* Runs one durable coding-agent Session until it settles.
|
||||||
|
|
@ -84,6 +85,7 @@ export const layer = Layer.effect(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const llm = yield* LLMClient.Service
|
const llm = yield* LLMClient.Service
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
const tools = yield* ToolRegistry.Service
|
const tools = yield* ToolRegistry.Service
|
||||||
const models = yield* SessionRunnerModel.Service
|
const models = yield* SessionRunnerModel.Service
|
||||||
const store = yield* SessionStore.Service
|
const store = yield* SessionStore.Service
|
||||||
|
|
@ -134,6 +136,7 @@ export const layer = Layer.effect(
|
||||||
const session = yield* getSession(sessionID)
|
const session = yield* getSession(sessionID)
|
||||||
const initialized = yield* SessionContextEpoch.initialize(db, systemContext, session.id, session.location)
|
const initialized = yield* SessionContextEpoch.initialize(db, systemContext, session.id, session.location)
|
||||||
const model = yield* models.resolve(session)
|
const model = yield* models.resolve(session)
|
||||||
|
const agent = yield* agents.resolve(session.agent)
|
||||||
const toolFibers = yield* FiberSet.make<void, never>()
|
const toolFibers = yield* FiberSet.make<void, never>()
|
||||||
let needsContinuation = false
|
let needsContinuation = false
|
||||||
if (promotion) {
|
if (promotion) {
|
||||||
|
|
@ -149,13 +152,13 @@ export const layer = Layer.effect(
|
||||||
const context = yield* store.runnerContext(session.id, system.baselineSeq)
|
const context = yield* store.runnerContext(session.id, system.baselineSeq)
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model,
|
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),
|
messages: toLLMMessages(context, model),
|
||||||
tools: yield* tools.definitions(),
|
tools: yield* tools.definitions(),
|
||||||
})
|
})
|
||||||
const publisher = createLLMEventPublisher(events, {
|
const publisher = createLLMEventPublisher(events, {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: session.agent ?? "build",
|
agent: agent?.id ?? "build",
|
||||||
model: {
|
model: {
|
||||||
id: ModelV2.ID.make(model.id),
|
id: ModelV2.ID.make(model.id),
|
||||||
providerID: ProviderV2.ID.make(model.provider),
|
providerID: ProviderV2.ID.make(model.provider),
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ const keys = new Set([
|
||||||
"disabled_providers",
|
"disabled_providers",
|
||||||
"enabled_providers",
|
"enabled_providers",
|
||||||
"small_model",
|
"small_model",
|
||||||
"default_agent",
|
|
||||||
"mode",
|
"mode",
|
||||||
"agent",
|
"agent",
|
||||||
"provider",
|
"provider",
|
||||||
|
|
@ -38,6 +37,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
|
||||||
$schema: info.$schema,
|
$schema: info.$schema,
|
||||||
shell: info.shell,
|
shell: info.shell,
|
||||||
model: info.model,
|
model: info.model,
|
||||||
|
default_agent: info.default_agent,
|
||||||
autoupdate: info.autoupdate,
|
autoupdate: info.autoupdate,
|
||||||
share: info.share ?? (info.autoshare ? "auto" : undefined),
|
share: info.share ?? (info.autoshare ? "auto" : undefined),
|
||||||
enterprise: info.enterprise,
|
enterprise: info.enterprise,
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,7 @@ describe("Config", () => {
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
shell: "/bin/bash",
|
shell: "/bin/bash",
|
||||||
model: "anthropic/claude",
|
model: "anthropic/claude",
|
||||||
|
default_agent: "reviewer",
|
||||||
autoupdate: "notify",
|
autoupdate: "notify",
|
||||||
share: "disabled",
|
share: "disabled",
|
||||||
enterprise: { url: "https://share.example.com" },
|
enterprise: { url: "https://share.example.com" },
|
||||||
|
|
@ -328,6 +329,7 @@ describe("Config", () => {
|
||||||
expect(documents).toHaveLength(1)
|
expect(documents).toHaveLength(1)
|
||||||
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
||||||
expect(documents[0]?.info.model).toBe("anthropic/claude")
|
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.autoupdate).toBe("notify")
|
||||||
expect(documents[0]?.info.share).toBe("disabled")
|
expect(documents[0]?.info.share).toBe("disabled")
|
||||||
expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" })
|
expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" })
|
||||||
|
|
@ -427,6 +429,7 @@ describe("Config", () => {
|
||||||
path.join(tmp.path, "opencode.json"),
|
path.join(tmp.path, "opencode.json"),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
shell: "/bin/zsh",
|
shell: "/bin/zsh",
|
||||||
|
default_agent: "reviewer",
|
||||||
snapshot: false,
|
snapshot: false,
|
||||||
autoshare: true,
|
autoshare: true,
|
||||||
permission: {
|
permission: {
|
||||||
|
|
@ -487,6 +490,7 @@ describe("Config", () => {
|
||||||
expect(documents).toHaveLength(1)
|
expect(documents).toHaveLength(1)
|
||||||
expect(documents[0]?.info).toBeInstanceOf(Config.Info)
|
expect(documents[0]?.info).toBeInstanceOf(Config.Info)
|
||||||
expect(documents[0]?.info.shell).toBe("/bin/zsh")
|
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.snapshots).toBe(false)
|
||||||
expect(documents[0]?.info.share).toBe("auto")
|
expect(documents[0]?.info.share).toBe("auto")
|
||||||
expect(documents[0]?.info.permissions).toEqual([
|
expect(documents[0]?.info.permissions).toEqual([
|
||||||
|
|
|
||||||
|
|
@ -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", () =>
|
it.effect("evaluates bash with the normal configured-rule semantics", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup([{ action: "*", resource: "*", effect: "allow" }])
|
yield* setup([{ action: "*", resource: "*", effect: "allow" }])
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||||
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
|
@ -48,6 +49,7 @@ const permission = Layer.succeed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||||
|
const agents = AgentV2.layer
|
||||||
const model = OpenAIChat.route
|
const model = OpenAIChat.route
|
||||||
.with({
|
.with({
|
||||||
endpoint: { baseURL: "https://api.openai.com/v1" },
|
endpoint: { baseURL: "https://api.openai.com/v1" },
|
||||||
|
|
@ -65,6 +67,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(models),
|
Layer.provide(models),
|
||||||
Layer.provide(systemContext),
|
Layer.provide(systemContext),
|
||||||
|
Layer.provide(agents),
|
||||||
)
|
)
|
||||||
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
||||||
const execution = Layer.effect(
|
const execution = Layer.effect(
|
||||||
|
|
@ -89,6 +92,7 @@ const it = testEffect(
|
||||||
executor,
|
executor,
|
||||||
client,
|
client,
|
||||||
permission,
|
permission,
|
||||||
|
agents,
|
||||||
registry,
|
registry,
|
||||||
models,
|
models,
|
||||||
systemContext,
|
systemContext,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
|
||||||
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
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 { NativeTool } from "@opencode-ai/core/tool/native"
|
||||||
import {
|
import {
|
||||||
SessionContextEpochTable,
|
SessionContextEpochTable,
|
||||||
|
|
@ -107,6 +108,7 @@ const permission = Layer.succeed(
|
||||||
)
|
)
|
||||||
const applications = ApplicationTools.layer
|
const applications = ApplicationTools.layer
|
||||||
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(applications))
|
const registry = ToolRegistry.layer.pipe(Layer.provide(permission), Layer.provide(applications))
|
||||||
|
const agents = AgentV2.layer
|
||||||
const echo = Layer.effectDiscard(
|
const echo = Layer.effectDiscard(
|
||||||
ToolRegistry.Service.use((registry) =>
|
ToolRegistry.Service.use((registry) =>
|
||||||
registry.contribute((editor) => {
|
registry.contribute((editor) => {
|
||||||
|
|
@ -189,6 +191,7 @@ const runner = SessionRunnerLLM.layer.pipe(
|
||||||
Layer.provide(registry),
|
Layer.provide(registry),
|
||||||
Layer.provide(models),
|
Layer.provide(models),
|
||||||
Layer.provide(systemContext),
|
Layer.provide(systemContext),
|
||||||
|
Layer.provide(agents),
|
||||||
)
|
)
|
||||||
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
||||||
const execution = Layer.effect(
|
const execution = Layer.effect(
|
||||||
|
|
@ -214,6 +217,7 @@ const it = testEffect(
|
||||||
client,
|
client,
|
||||||
permission,
|
permission,
|
||||||
applications,
|
applications,
|
||||||
|
agents,
|
||||||
registry,
|
registry,
|
||||||
echo,
|
echo,
|
||||||
models,
|
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", () =>
|
it.effect("admits removed context as a chronological System message", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue