From da689d77c46e58e5d6bc269f64ca6c804d1dcf43 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 12 May 2026 21:45:53 -0400 Subject: [PATCH] effect: move tool flags into RuntimeFlags (#27198) --- packages/opencode/src/effect/runtime-flags.ts | 23 +++++++- packages/opencode/src/tool/registry.ts | 21 ++++--- packages/opencode/src/tool/websearch.ts | 13 +++-- .../test/effect/runtime-flags.test.ts | 19 +++++++ packages/opencode/test/session/prompt.test.ts | 2 + .../test/session/snapshot-tool-race.test.ts | 2 + packages/opencode/test/tool/registry.test.ts | 55 +++++++++---------- 7 files changed, 88 insertions(+), 47 deletions(-) diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 5f07dc6acc..3dfb2e99f4 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -1,9 +1,28 @@ import { Config, ConfigProvider, Context, Effect, Layer } from "effect" import { ConfigService } from "@/effect/config-service" +const bool = (name: string) => Config.boolean(name).pipe(Config.withDefault(false)) +const experimental = bool("OPENCODE_EXPERIMENTAL") +const enabledByExperimental = (name: string) => + Config.all({ experimental, enabled: bool(name) }).pipe(Config.map((flags) => flags.experimental || flags.enabled)) + export class Service extends ConfigService.Service()("@opencode/RuntimeFlags", { - pure: Config.boolean("OPENCODE_PURE").pipe(Config.withDefault(false)), - disableDefaultPlugins: Config.boolean("OPENCODE_DISABLE_DEFAULT_PLUGINS").pipe(Config.withDefault(false)), + pure: bool("OPENCODE_PURE"), + disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"), + enableExa: Config.all({ + experimental, + enabled: bool("OPENCODE_ENABLE_EXA"), + legacy: bool("OPENCODE_EXPERIMENTAL_EXA"), + }).pipe(Config.map((flags) => flags.experimental || flags.enabled || flags.legacy)), + enableParallel: Config.all({ + enabled: bool("OPENCODE_ENABLE_PARALLEL"), + legacy: bool("OPENCODE_EXPERIMENTAL_PARALLEL"), + }).pipe(Config.map((flags) => flags.enabled || flags.legacy)), + enableQuestionTool: bool("OPENCODE_ENABLE_QUESTION_TOOL"), + experimentalScout: enabledByExperimental("OPENCODE_EXPERIMENTAL_SCOUT"), + experimentalLspTool: enabledByExperimental("OPENCODE_EXPERIMENTAL_LSP_TOOL"), + experimentalPlanMode: enabledByExperimental("OPENCODE_EXPERIMENTAL_PLAN_MODE"), + client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")), }) {} export type Info = Context.Service.Shape diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 7de3c8f4e8..cb3e4ce6bf 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -24,7 +24,6 @@ import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { RepoCloneTool } from "./repo_clone" import { RepoOverviewTool } from "./repo_overview" -import { Flag } from "@opencode-ai/core/flag/flag" import * as Log from "@opencode-ai/core/util/log" import { LspTool } from "./lsp" import * as Truncate from "./truncate" @@ -50,13 +49,11 @@ import { Git } from "@/git" import { Skill } from "../skill" import { Permission } from "@/permission" import { Reference } from "@/reference/reference" +import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "tool.registry" }) -export function webSearchEnabled( - providerID: ProviderID, - flags = { exa: Flag.OPENCODE_ENABLE_EXA, parallel: Flag.OPENCODE_ENABLE_PARALLEL }, -) { +export function webSearchEnabled(providerID: ProviderID, flags = { exa: false, parallel: false }) { return providerID === ProviderID.opencode || flags.exa || flags.parallel } @@ -101,6 +98,7 @@ export const layer: Layer.Layer< | Ripgrep.Service | Format.Service | Truncate.Service + | RuntimeFlags.Service > = Layer.effect( Service, Effect.gen(function* () { @@ -109,6 +107,7 @@ export const layer: Layer.Layer< const agents = yield* Agent.Service const skill = yield* Skill.Service const truncate = yield* Truncate.Service + const flags = yield* RuntimeFlags.Service const invalid = yield* InvalidTool const task = yield* TaskTool @@ -209,8 +208,7 @@ export const layer: Layer.Layer< } yield* config.get() - const questionEnabled = - ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL + const questionEnabled = ["app", "cli", "desktop"].includes(flags.client) || flags.enableQuestionTool const tool = yield* Effect.all({ invalid: Tool.init(invalid), @@ -248,11 +246,11 @@ export const layer: Layer.Layer< tool.fetch, tool.todo, tool.search, - ...(Flag.OPENCODE_EXPERIMENTAL_SCOUT ? [tool.repo_clone, tool.repo_overview] : []), + ...(flags.experimentalScout ? [tool.repo_clone, tool.repo_overview] : []), tool.skill, tool.patch, - ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []), + ...(flags.experimentalLspTool ? [tool.lsp] : []), + ...(flags.experimentalPlanMode && flags.client === "cli" ? [tool.plan] : []), ], task: tool.task, read: tool.read, @@ -306,7 +304,7 @@ export const layer: Layer.Layer< const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) { const filtered = (yield* all()).filter((tool) => { if (tool.id === WebSearchTool.id) { - return webSearchEnabled(input.providerID) + return webSearchEnabled(input.providerID, { exa: flags.enableExa, parallel: flags.enableParallel }) } const usePatch = @@ -380,6 +378,7 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(CrossSpawnSpawner.defaultLayer), Layer.provide(Ripgrep.defaultLayer), Layer.provide(Truncate.defaultLayer), + Layer.provide(RuntimeFlags.defaultLayer), ), ) diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts index 0218ecbe3b..d08ae1d153 100644 --- a/packages/opencode/src/tool/websearch.ts +++ b/packages/opencode/src/tool/websearch.ts @@ -3,9 +3,9 @@ import { HttpClient } from "effect/unstable/http" import * as Tool from "./tool" import * as McpWebSearch from "./mcp-websearch" import DESCRIPTION from "./websearch.txt" -import { Flag } from "@opencode-ai/core/flag/flag" import { checksum } from "@opencode-ai/core/util/encode" import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { RuntimeFlags } from "@/effect/runtime-flags" export const Parameters = Schema.Struct({ query: Schema.String.annotate({ description: "Websearch query" }), @@ -27,10 +27,7 @@ export const Parameters = Schema.Struct({ const WebSearchProviderSchema = Schema.Literals(["exa", "parallel"]) export type WebSearchProvider = Schema.Schema.Type -export function selectWebSearchProvider( - sessionID: string, - flags = { exa: Flag.OPENCODE_ENABLE_EXA, parallel: Flag.OPENCODE_ENABLE_PARALLEL }, -): WebSearchProvider { +export function selectWebSearchProvider(sessionID: string, flags = { exa: false, parallel: false }): WebSearchProvider { const override = process.env.OPENCODE_WEBSEARCH_PROVIDER if (override === "exa" || override === "parallel") return override if (flags.parallel) return "parallel" @@ -103,6 +100,7 @@ export const WebSearchTool = Tool.define( "websearch", Effect.gen(function* () { const http = yield* HttpClient.HttpClient + const flags = yield* RuntimeFlags.Service return { get description() { @@ -111,7 +109,10 @@ export const WebSearchTool = Tool.define( parameters: Parameters, execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { - const provider = selectWebSearchProvider(ctx.sessionID) + const provider = selectWebSearchProvider(ctx.sessionID, { + exa: flags.enableExa, + parallel: flags.enableParallel, + }) const title = webSearchProviderLabel(provider) yield* ctx.metadata({ title: `${title} "${params.query}"`, metadata: { provider } }) diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 5c9518a271..058fa7559a 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -16,12 +16,24 @@ describe("RuntimeFlags", () => { fromConfig({ OPENCODE_PURE: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + OPENCODE_EXPERIMENTAL: "true", + OPENCODE_ENABLE_EXA: "true", + OPENCODE_ENABLE_PARALLEL: "true", + OPENCODE_ENABLE_QUESTION_TOOL: "true", + OPENCODE_CLIENT: "desktop", }), ), ) expect(flags.pure).toBe(true) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.enableExa).toBe(true) + expect(flags.enableParallel).toBe(true) + expect(flags.enableQuestionTool).toBe(true) + expect(flags.experimentalScout).toBe(true) + expect(flags.experimentalLspTool).toBe(true) + expect(flags.experimentalPlanMode).toBe(true) + expect(flags.client).toBe("desktop") }), ) @@ -31,6 +43,8 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.enableExa).toBe(false) + expect(flags.client).toBe("cli") }), ) @@ -43,6 +57,9 @@ describe("RuntimeFlags", () => { ConfigProvider.fromUnknown({ OPENCODE_PURE: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + OPENCODE_EXPERIMENTAL: "true", + OPENCODE_ENABLE_EXA: "true", + OPENCODE_CLIENT: "desktop", }), ), ), @@ -50,6 +67,8 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.disableDefaultPlugins).toBe(false) + expect(flags.enableExa).toBe(false) + expect(flags.client).toBe("cli") }), ) }) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 3821954945..078a8d3bb0 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -52,6 +52,7 @@ import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { reply, TestLLMServer } from "../lib/llm-server" import { SyncEvent } from "@/sync" +import { RuntimeFlags } from "@/effect/runtime-flags" void Log.init({ print: false }) @@ -188,6 +189,7 @@ function makeHttp() { Layer.provide(Reference.defaultLayer), Layer.provide(Ripgrep.defaultLayer), Layer.provide(Format.defaultLayer), + Layer.provide(RuntimeFlags.layer()), Layer.provideMerge(todo), Layer.provideMerge(question), Layer.provideMerge(deps), diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 8640612e98..adf8926870 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -59,6 +59,7 @@ import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { Reference } from "../../src/reference/reference" import { SyncEvent } from "@/sync" +import { RuntimeFlags } from "@/effect/runtime-flags" void Log.init({ print: false }) @@ -137,6 +138,7 @@ function makeHttp() { Layer.provide(Reference.defaultLayer), Layer.provide(Ripgrep.defaultLayer), Layer.provide(Format.defaultLayer), + Layer.provide(RuntimeFlags.layer()), Layer.provideMerge(todo), Layer.provideMerge(question), Layer.provideMerge(deps), diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index b2beda70ca..f00b7f5a08 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -6,7 +6,6 @@ import { Effect, Layer, Result, Schema } from "effect" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { ToolRegistry } from "@/tool/registry" import { Tool } from "@/tool/tool" -import { Flag } from "@opencode-ai/core/flag/flag" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { TestConfig } from "../fixture/config" @@ -31,46 +30,47 @@ import { Reference } from "@/reference/reference" import { ProviderID, ModelID } from "@/provider/schema" import { ToolJsonSchema } from "@/tool/json-schema" import { MessageID, SessionID } from "@/session/schema" +import { RuntimeFlags } from "@/effect/runtime-flags" const node = CrossSpawnSpawner.defaultLayer -const originalExperimentalScout = Flag.OPENCODE_EXPERIMENTAL_SCOUT const configLayer = TestConfig.layer({ directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])), }) -const registryLayer = ToolRegistry.layer.pipe( - Layer.provide(configLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(Question.defaultLayer), - Layer.provide(Todo.defaultLayer), - Layer.provide(Skill.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(Provider.defaultLayer), - Layer.provide(Git.defaultLayer), - Layer.provide(Reference.defaultLayer), - Layer.provide(LSP.defaultLayer), - Layer.provide(Instruction.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(FetchHttpClient.layer), - Layer.provide(Format.defaultLayer), - Layer.provide(node), - Layer.provide(Ripgrep.defaultLayer), - Layer.provide(Truncate.defaultLayer), -) +const registryLayer = (flags: Partial = {}) => + ToolRegistry.layer.pipe( + Layer.provide(configLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(Question.defaultLayer), + Layer.provide(Todo.defaultLayer), + Layer.provide(Skill.defaultLayer), + Layer.provide(Agent.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(Provider.defaultLayer), + Layer.provide(Git.defaultLayer), + Layer.provide(Reference.defaultLayer), + Layer.provide(LSP.defaultLayer), + Layer.provide(Instruction.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Bus.layer), + Layer.provide(FetchHttpClient.layer), + Layer.provide(Format.defaultLayer), + Layer.provide(node), + Layer.provide(Ripgrep.defaultLayer), + Layer.provide(Truncate.defaultLayer), + Layer.provide(RuntimeFlags.layer(flags)), + ) -const it = testEffect(Layer.mergeAll(registryLayer, node, Agent.defaultLayer)) +const it = testEffect(Layer.mergeAll(registryLayer(), node, Agent.defaultLayer)) +const scout = testEffect(Layer.mergeAll(registryLayer({ experimentalScout: true }), node, Agent.defaultLayer)) afterEach(async () => { - Flag.OPENCODE_EXPERIMENTAL_SCOUT = originalExperimentalScout await disposeAllInstances() }) describe("tool.registry", () => { it.instance("hides repo research tools unless experimental", () => Effect.gen(function* () { - Flag.OPENCODE_EXPERIMENTAL_SCOUT = false const registry = yield* ToolRegistry.Service const ids = yield* registry.ids() @@ -79,9 +79,8 @@ describe("tool.registry", () => { }), ) - it.instance("shows repo research tools when experimental scout is enabled", () => + scout.instance("shows repo research tools when experimental scout is enabled", () => Effect.gen(function* () { - Flag.OPENCODE_EXPERIMENTAL_SCOUT = true const registry = yield* ToolRegistry.Service const ids = yield* registry.ids()