refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
commit 8db7487c89
466 changed files with 9405 additions and 11071 deletions

View file

@ -2,36 +2,35 @@ import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Config } from "@opencode-ai/schema/config"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Money } from "@opencode-ai/schema/money"
import { DateTime, Deferred, Effect, Equal, Fiber, Hash, RcMap, Schema, Stream } from "effect"
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
import { Agent } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { Plugin } from "@opencode-ai/core/plugin"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Model } from "@opencode-ai/core/model"
import { Project } from "@opencode-ai/core/project"
import { Provider } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { Session } from "@opencode-ai/core/session"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
import { toolDefinitions, waitForTool } from "./lib/tool"
import { Database } from "../src/database/database"
import { EventV2 } from "../src/event"
import { Bus } from "../src/bus"
import { Reference } from "../src/reference"
import { ToolRegistry } from "../src/tool/registry"
import { Tool } from "../src/tool"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, LocationServiceMap.node])))
const itWithSdk = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])),
)
describe("LocationServiceMap", () => {
@ -44,7 +43,7 @@ describe("LocationServiceMap", () => {
Effect.gen(function* () {
const sdk = yield* SdkPlugins.Service
const locations = yield* LocationServiceMap.Service
const id = AgentV2.ID.make("persistent-sdk-agent")
const id = Agent.ID.make("persistent-sdk-agent")
const plugin = EffectPlugin.define({
id: "persistent-sdk-plugin",
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
@ -55,7 +54,7 @@ describe("LocationServiceMap", () => {
const read = Effect.gen(function* () {
const supervisor = yield* PluginSupervisor.Service
yield* supervisor.flush
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
return yield* agents.get(id)
})
@ -101,7 +100,7 @@ describe("LocationServiceMap", () => {
)
const explorer = yield* Effect.gen(function* () {
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
return yield* agents.resolve("explore")
}).pipe(Effect.provide(context))
@ -193,8 +192,8 @@ describe("LocationServiceMap", () => {
const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))
yield* Deferred.await(firstStarted)
const events = yield* EventV2.Service
const updated = yield* events.subscribe(Config.Event.Updated).pipe(
const bus = yield* Bus.Service
const updated = yield* bus.subscribe(Config.Event.Updated).pipe(
Stream.filter((event) => event.location?.directory === dir.path),
Stream.runHead,
Effect.forkChild({ startImmediately: true }),
@ -235,11 +234,11 @@ describe("LocationServiceMap", () => {
Effect.provide(context),
Effect.forkChild({ startImmediately: true }),
)
const events = yield* EventV2.Service
const bus = yield* Bus.Service
yield* Effect.forEach(
Array.from({ length: 5 }),
() => events.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))),
() => bus.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))),
{ discard: true },
)
expect(flushFiber.pollUnsafe()).toBeUndefined()
@ -270,7 +269,7 @@ describe("LocationServiceMap", () => {
yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(context))
expect(activations.count).toBe(1)
yield* EventV2.Service.use((events) => events.publish(Config.Event.Updated, {})).pipe(Effect.provide(context))
yield* Bus.Service.use((bus) => bus.publish(Config.Event.Updated, {})).pipe(Effect.provide(context))
yield* Effect.sleep("200 millis")
expect(activations.count).toBe(1)
@ -370,7 +369,7 @@ describe("LocationServiceMap", () => {
fs.writeFile(path.join(dir.path, "opencode.json"), JSON.stringify({ plugins: ["-*", "opencode.agent"] })),
)
const plugins = yield* Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const plugins = yield* Plugin.Service
yield* (yield* PluginSupervisor.Service).flush
return yield* plugins.list()
}).pipe(
@ -396,7 +395,7 @@ describe("LocationServiceMap", () => {
const file = path.join(dir.path, "opencode.json")
yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] })))
yield* Effect.gen(function* () {
const registry = yield* PluginV2.Service
const registry = yield* Plugin.Service
const supervisor = yield* PluginSupervisor.Service
yield* supervisor.flush
expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"])
@ -449,25 +448,25 @@ describe("LocationServiceMap", () => {
Effect.scoped(
Effect.gen(function* () {
const locations = yield* LocationServiceMap.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const firstRef = Location.Ref.make({ directory: AbsolutePath.make(first.path) })
const secondRef = Location.Ref.make({ directory: AbsolutePath.make(second.path) })
const firstContext = yield* locations.contextEffect(firstRef)
const secondContext = yield* locations.contextEffect(secondRef)
const received = { first: 0, second: 0 }
yield* events.subscribe(Config.Event.Updated).pipe(
yield* bus.subscribe(Config.Event.Updated).pipe(
Stream.runForEach(() => Effect.sync(() => received.first++)),
Effect.provideContext(firstContext),
Effect.forkScoped({ startImmediately: true }),
)
yield* events.subscribe(Config.Event.Updated).pipe(
yield* bus.subscribe(Config.Event.Updated).pipe(
Stream.runForEach(() => Effect.sync(() => received.second++)),
Effect.provideContext(secondContext),
Effect.forkScoped({ startImmediately: true }),
)
yield* Effect.sleep("10 millis")
yield* events.publish(Config.Event.Updated, {}, { location: firstRef })
yield* bus.publish(Config.Event.Updated, {}, { location: firstRef })
yield* Effect.sleep("10 millis")
expect(received).toEqual({ first: 1, second: 0 })
@ -538,12 +537,12 @@ describe("LocationServiceMap", () => {
).pipe(
Effect.flatMap(([blocked, allowed]) =>
Effect.gen(function* () {
const update = (directory: string, providerID: ProviderV2.ID) =>
const update = (directory: string, providerID: Provider.ID) =>
Effect.gen(function* () {
yield* Reference.Service
const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
const registry = yield* ToolRegistry.Service
const registry = yield* Tool.Service
// Tool plugins register during the forked PluginSupervisor boot; wait for
// every expected tool rather than relying on batch ordering.
yield* Effect.forEach(
@ -573,8 +572,8 @@ describe("LocationServiceMap", () => {
),
)
const blockedID = ProviderV2.ID.make("blocked-location")
const allowedID = ProviderV2.ID.make("allowed-location")
const blockedID = Provider.ID.make("blocked-location")
const allowedID = Provider.ID.make("allowed-location")
const blockedState = yield* update(blocked.path, blockedID)
expect(blockedState.providers.some((provider) => provider.id === blockedID)).toBe(true)
expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false)
@ -641,13 +640,13 @@ describe("LocationServiceMap", () => {
)
const failure = yield* SessionRunnerModel.Service.use((models) =>
models.resolve(
SessionV2.Info.make({
id: SessionV2.ID.make("ses_unavailable_model"),
projectID: ProjectV2.ID.global,
Session.Info.make({
id: Session.ID.make("ses_unavailable_model"),
projectID: Project.ID.global,
title: "test",
model: {
id: ModelV2.ID.make("chat"),
providerID: ProviderV2.ID.make("unavailable"),
id: Model.ID.make("chat"),
providerID: Provider.ID.make("unavailable"),
},
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
@ -678,25 +677,25 @@ describe("LocationServiceMap", () => {
const resolved = yield* Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => {
editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => {
provider.package = ProviderV2.aisdk("@ai-sdk/openai")
editor.provider.update(Provider.ID.make("aliased"), (provider) => {
provider.package = Provider.aisdk("@ai-sdk/openai")
})
editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => {
editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => {
// Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5.
model.modelID = ModelV2.ID.make("base")
model.variants = [{ id: ModelV2.VariantID.make("high") }]
model.modelID = Model.ID.make("base")
model.variants = [{ id: Model.VariantID.make("high") }]
})
})
const models = yield* SessionRunnerModel.Service
return yield* models.resolve(
SessionV2.Info.make({
id: SessionV2.ID.make("ses_aliased_model"),
projectID: ProjectV2.ID.global,
Session.Info.make({
id: Session.ID.make("ses_aliased_model"),
projectID: Project.ID.global,
title: "test",
model: {
id: ModelV2.ID.make("fast"),
providerID: ProviderV2.ID.make("aliased"),
variant: ModelV2.VariantID.make("high"),
id: Model.ID.make("fast"),
providerID: Provider.ID.make("aliased"),
variant: Model.VariantID.make("high"),
},
cost: Money.USD.zero,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
@ -707,10 +706,10 @@ describe("LocationServiceMap", () => {
}).pipe(Effect.provide(LocationServiceMap.Service.get(location)))
expect(resolved.ref).toEqual(
ModelV2.Ref.make({
id: ModelV2.ID.make("fast"),
providerID: ProviderV2.ID.make("aliased"),
variant: ModelV2.VariantID.make("high"),
Model.Ref.make({
id: Model.ID.make("fast"),
providerID: Provider.ID.make("aliased"),
variant: Model.VariantID.make("high"),
}),
)
expect(String(resolved.model.id)).toBe("base")
@ -726,7 +725,7 @@ describe("LocationServiceMap", () => {
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const plugins = yield* Plugin.Service
const reviewer = EffectPlugin.define({
id: "reviewer",
effect: (ctx) =>
@ -741,7 +740,7 @@ describe("LocationServiceMap", () => {
})
yield* plugins.activate([{ ...reviewer, version: "1" }])
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
expect(yield* (yield* Agent.Service).get(Agent.ID.make("reviewer"))).toMatchObject({
description: "Reviews code",
mode: "subagent",
})