diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts index fc3573548d3..33170c3261e 100644 --- a/packages/opencode/script/seed-e2e.ts +++ b/packages/opencode/script/seed-e2e.ts @@ -11,7 +11,7 @@ const seed = async () => { const { Instance } = await import("../src/project/instance") const { InstanceBootstrap } = await import("../src/project/bootstrap") const { Config } = await import("../src/config/config") - const { disposeRuntime } = await import("../src/effect/runtime") + const { disposeAllRuntimes } = await import("../src/effect/runtime") const { Session } = await import("../src/session") const { MessageID, PartID } = await import("../src/session/schema") const { Project } = await import("../src/project/project") @@ -55,7 +55,7 @@ const seed = async () => { }) } finally { await Instance.disposeAll().catch(() => {}) - await disposeRuntime().catch(() => {}) + await disposeAllRuntimes().catch(() => {}) } } diff --git a/packages/opencode/src/account/effect.ts b/packages/opencode/src/account/effect.ts index 2f1304d505b..c5cb0166f9a 100644 --- a/packages/opencode/src/account/effect.ts +++ b/packages/opencode/src/account/effect.ts @@ -23,6 +23,7 @@ import { PollSuccess, UserCode, } from "./schema" +import { makeRuntimeGlobal } from "@/effect/runtime" export * from "./schema" @@ -357,4 +358,6 @@ export namespace AccountEffect { ) export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer)) + + export const { runtime, runSync, runPromise } = makeRuntimeGlobal(Service, defaultLayer) } diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 3a9d758e2f1..4ced47b6182 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -1,41 +1,24 @@ -import { Effect, Option } from "effect" +import { Option } from "effect" -import { - Account as AccountSchema, - type AccountError, - type AccessToken, - AccountID, - AccountEffect, - OrgID, -} from "./effect" +import { Account as AccountSchema, type AccessToken, AccountID, OrgID, AccountEffect } from "./effect" export { AccessToken, AccountID, OrgID } from "./effect" -import { runtime } from "@/effect/runtime" - -function runSync(f: (service: AccountEffect.Interface) => Effect.Effect) { - return runtime.runSync(AccountEffect.Service.use(f)) -} - -function runPromise(f: (service: AccountEffect.Interface) => Effect.Effect) { - return runtime.runPromise(AccountEffect.Service.use(f)) -} - export namespace Account { export const Account = AccountSchema export type Account = AccountSchema export function active(): Account | undefined { - return Option.getOrUndefined(runSync((service) => service.active())) + return Option.getOrUndefined(AccountEffect.runSync((service) => service.active())) } export async function config(accountID: AccountID, orgID: OrgID): Promise | undefined> { - const config = await runPromise((service) => service.config(accountID, orgID)) + const config = await AccountEffect.runPromise((service) => service.config(accountID, orgID)) return Option.getOrUndefined(config) } export async function token(accountID: AccountID): Promise { - const token = await runPromise((service) => service.token(accountID)) + const token = await AccountEffect.runPromise((service) => service.token(accountID)) return Option.getOrUndefined(token) } } diff --git a/packages/opencode/src/auth/effect.ts b/packages/opencode/src/auth/effect.ts index e03ad958674..6c43ea28d27 100644 --- a/packages/opencode/src/auth/effect.ts +++ b/packages/opencode/src/auth/effect.ts @@ -1,5 +1,6 @@ import path from "path" import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect" +import { makeRuntimeGlobal } from "@/effect/runtime" import { Global } from "../global" import { Filesystem } from "../util/filesystem" @@ -91,4 +92,6 @@ export namespace AuthEffect { return Service.of({ get, all, set, remove }) }), ) + + export const { runtime, runSync, runPromise } = makeRuntimeGlobal(Service, layer) } diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 6f588e93751..95f6758a267 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,14 +1,8 @@ -import { Effect } from "effect" import z from "zod" -import { runtime } from "@/effect/runtime" import * as S from "./effect" export { OAUTH_DUMMY_KEY } from "./effect" -function runPromise(f: (service: S.AuthEffect.Interface) => Effect.Effect) { - return runtime.runPromise(S.AuthEffect.Service.use(f)) -} - export namespace Auth { export const Oauth = z .object({ @@ -40,18 +34,18 @@ export namespace Auth { export type Info = z.infer export async function get(providerID: string) { - return runPromise((service) => service.get(providerID)) + return S.AuthEffect.runPromise((service) => service.get(providerID)) } export async function all(): Promise> { - return runPromise((service) => service.all()) + return S.AuthEffect.runPromise((service) => service.all()) } export async function set(key: string, info: Info) { - return runPromise((service) => service.set(key, info)) + return S.AuthEffect.runPromise((service) => service.set(key, info)) } export async function remove(key: string) { - return runPromise((service) => service.remove(key)) + return S.AuthEffect.runPromise((service) => service.remove(key)) } } diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index c2b47da11c3..fd66c3fdc99 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -1,7 +1,6 @@ import { cmd } from "./cmd" import { Duration, Effect, Match, Option } from "effect" import { UI } from "../ui" -import { runtime } from "@/effect/runtime" import { AccountID, AccountEffect, OrgID, PollExpired, type PollResult } from "@/account/effect" import { type AccountError } from "@/account/schema" import * as Prompt from "../effect/prompt" @@ -160,7 +159,7 @@ export const LoginCommand = cmd({ }), async handler(args) { UI.empty() - await runtime.runPromise(loginEffect(args.url)) + await AccountEffect.runtime().runPromise(loginEffect(args.url)) }, }) @@ -174,7 +173,7 @@ export const LogoutCommand = cmd({ }), async handler(args) { UI.empty() - await runtime.runPromise(logoutEffect(args.email)) + await AccountEffect.runtime().runPromise(logoutEffect(args.email)) }, }) @@ -183,7 +182,7 @@ export const SwitchCommand = cmd({ describe: false, async handler() { UI.empty() - await runtime.runPromise(switchEffect()) + await AccountEffect.runtime().runPromise(switchEffect()) }, }) @@ -192,7 +191,7 @@ export const OrgsCommand = cmd({ describe: false, async handler() { UI.empty() - await runtime.runPromise(orgsEffect()) + await AccountEffect.runtime().runPromise(orgsEffect()) }, }) diff --git a/packages/opencode/src/effect/instance-registry.ts b/packages/opencode/src/effect/instance-registry.ts deleted file mode 100644 index 59c556e0447..00000000000 --- a/packages/opencode/src/effect/instance-registry.ts +++ /dev/null @@ -1,12 +0,0 @@ -const disposers = new Set<(directory: string) => Promise>() - -export function registerDisposer(disposer: (directory: string) => Promise) { - disposers.add(disposer) - return () => { - disposers.delete(disposer) - } -} - -export async function disposeInstance(directory: string) { - await Promise.allSettled([...disposers].map((disposer) => disposer(directory))) -} diff --git a/packages/opencode/src/effect/instances.ts b/packages/opencode/src/effect/instances.ts deleted file mode 100644 index c05458d5df9..00000000000 --- a/packages/opencode/src/effect/instances.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Effect, Layer, LayerMap, ServiceMap } from "effect" -import { File } from "@/file" -import { FileTime } from "@/file/time" -import { FileWatcher } from "@/file/watcher" -import { Format } from "@/format" -import { PermissionNext } from "@/permission" -import { Instance } from "@/project/instance" -import { Vcs } from "@/project/vcs" -import { ProviderAuth } from "@/provider/auth" -import { Question } from "@/question" -import { Skill } from "@/skill/skill" -import { Snapshot } from "@/snapshot" -import { InstanceContext } from "./instance-context" -import { registerDisposer } from "./instance-registry" - -export { InstanceContext } from "./instance-context" - -export type InstanceServices = - | Question.Service - | PermissionNext.Service - | ProviderAuth.Service - | FileWatcher.Service - | Vcs.Service - | FileTime.Service - | Format.Service - | File.Service - | Skill.Service - | Snapshot.Service - -// NOTE: LayerMap only passes the key (directory string) to lookup, but we need -// the full instance context (directory, worktree, project). We read from the -// legacy Instance ALS here, which is safe because lookup is only triggered via -// runPromiseInstance -> Instances.get, which always runs inside Instance.provide. -// This should go away once the old Instance type is removed and lookup can load -// the full context directly. -function lookup(_key: string) { - const ctx = Layer.sync(InstanceContext, () => InstanceContext.of(Instance.current)) - return Layer.mergeAll( - Layer.fresh(Question.layer), - Layer.fresh(PermissionNext.layer), - Layer.fresh(ProviderAuth.defaultLayer), - Layer.fresh(FileWatcher.layer).pipe(Layer.orDie), - Layer.fresh(Vcs.layer), - Layer.fresh(FileTime.layer).pipe(Layer.orDie), - Layer.fresh(Format.layer), - Layer.fresh(File.layer), - Layer.fresh(Skill.defaultLayer), - Layer.fresh(Snapshot.defaultLayer), - ).pipe(Layer.provide(ctx)) -} - -export class Instances extends ServiceMap.Service>()( - "opencode/Instances", -) { - static readonly layer = Layer.effect( - Instances, - Effect.gen(function* () { - const layerMap = yield* LayerMap.make(lookup, { idleTimeToLive: Infinity }) - const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory))) - yield* Effect.addFinalizer(() => Effect.sync(unregister)) - return Instances.of(layerMap) - }), - ) - - static get(directory: string): Layer.Layer { - return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory)))) - } -} diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts index f52203b2220..9b34eaca6c8 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -1,23 +1,103 @@ import { Effect, Layer, ManagedRuntime } from "effect" -import { AccountEffect } from "@/account/effect" -import { AuthEffect } from "@/auth/effect" -import { Instances } from "@/effect/instances" -import type { InstanceServices } from "@/effect/instances" -import { TruncateEffect } from "@/tool/truncate-effect" +import * as ServiceMap from "effect/ServiceMap" +import { InstanceContext } from "./instance-context" import { Instance } from "@/project/instance" +import * as Exit from "effect/Exit" +import * as Cause from "effect/Cause" -export const runtime = ManagedRuntime.make( - Layer.mergeAll( - AccountEffect.defaultLayer, // - TruncateEffect.defaultLayer, - Instances.layer, - ).pipe(Layer.provideMerge(AuthEffect.layer)), -) +export const opencodeMemoMap = Layer.makeMemoMapUnsafe() -export function runPromiseInstance(effect: Effect.Effect) { - return runtime.runPromise(effect.pipe(Effect.provide(Instances.get(Instance.directory)))) +export interface ServiceRuntime { + readonly runtime: () => ManagedRuntime.ManagedRuntime + readonly runSync: (f: (service: S) => Effect.Effect) => A + readonly runPromise: (f: (service: S) => Effect.Effect, options?: Effect.RunOptions) => Promise } -export function disposeRuntime() { - return runtime.dispose() +export const makeRuntimeGlobal = ( + service: ServiceMap.Service, + layer: Layer.Layer, +): ServiceRuntime => { + let runtime = globalRuntimes.get(layer) as ManagedRuntime.ManagedRuntime | undefined + if (!runtime) { + runtime = ManagedRuntime.make(layer, { memoMap: opencodeMemoMap }) + globalRuntimes.set(layer, runtime) + } + const runSync = (f: (service: S) => Effect.Effect) => runtime.runSync(service.use(f)) + const runPromise = (f: (service: S) => Effect.Effect) => runtime.runPromise(service.use(f)) + return { runtime: () => runtime, runSync, runPromise } +} +const globalRuntimes = new Map, ManagedRuntime.ManagedRuntime>() + +export const makeRuntimeInstance = ( + service: ServiceMap.Service, + layer: Layer.Layer, +): ServiceRuntime => { + const runSync = (f: (service: S) => Effect.Effect) => getInstanceRuntime(layer).runSync(service.use(f)) + const runPromise = (f: (service: S) => Effect.Effect, options?: Effect.RunOptions) => + new Promise((resolve, reject) => { + const fiber = getInstanceRuntime(layer).runFork(service.use(f), options) + fiber.addObserver((exit) => { + if (Exit.isSuccess(exit)) { + return resolve(exit.value) + } else if (Cause.hasInterruptsOnly(exit.cause)) { + return + } + reject(Cause.squash(exit.cause)) + }) + }) + return { runtime: () => getInstanceRuntime(layer), runSync, runPromise } +} + +const allRuntimes = new Map, ManagedRuntime.ManagedRuntime>>() + +const getInstanceRuntime = ( + layer: Layer.Layer, +): ManagedRuntime.ManagedRuntime => { + const directory = Instance.directory + + let map = allRuntimes.get(directory) + if (!map) { + map = new Map() + allRuntimes.set(directory, map) + } + + let runtime = map.get(layer) as ManagedRuntime.ManagedRuntime | undefined + if (!runtime) { + runtime = ManagedRuntime.make( + Layer.provideMerge( + layer, + Layer.sync(InstanceContext, () => InstanceContext.of(Instance.current)), + ), + { memoMap: opencodeMemoMap }, + ) + map.set(layer, runtime) + } + + return runtime +} + +export async function disposeAllRuntimes() { + let promises: Promise[] = [] + for (const runtime of globalRuntimes.values()) { + promises.push(runtime.dispose()) + } + globalRuntimes.clear() + for (const map of allRuntimes.values()) { + for (const runtime of map.values()) { + promises.push(runtime.dispose()) + } + } + allRuntimes.clear() + await Promise.all(promises) +} + +export const disposeInstanceRuntimes = async (directory: string) => { + const map = allRuntimes.get(directory) + if (!map) return + let promises: Promise[] = [] + for (const runtime of map.values()) { + promises.push(runtime.dispose()) + } + allRuntimes.delete(directory) + await Promise.all(promises) } diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 6e9b917271f..c73e3dbf89e 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,6 +1,6 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceContext } from "@/effect/instance-context" -import { runPromiseInstance } from "@/effect/runtime" +import { makeRuntimeInstance } from "@/effect/runtime" import { git } from "@/util/git" import { Effect, Fiber, Layer, Scope, ServiceMap } from "effect" import { formatPatch, structuredPatch } from "diff" @@ -84,23 +84,23 @@ export namespace File { } export function init() { - return runPromiseInstance(Service.use((svc) => svc.init())) + return runPromise((svc) => svc.init()) } export async function status() { - return runPromiseInstance(Service.use((svc) => svc.status())) + return runPromise((svc) => svc.status()) } export async function read(file: string): Promise { - return runPromiseInstance(Service.use((svc) => svc.read(file))) + return runPromise((svc) => svc.read(file)) } export async function list(dir?: string) { - return runPromiseInstance(Service.use((svc) => svc.list(dir))) + return runPromise((svc) => svc.list(dir)) } export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) { - return runPromiseInstance(Service.use((svc) => svc.search(input))) + return runPromise((svc) => svc.search(input)) } const log = Log.create({ service: "file" }) @@ -692,4 +692,6 @@ export namespace File { return Service.of({ init, status, read, list, search }) }), ) + + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) } diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 3d94bc12221..1f5f07ca9f2 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,5 +1,5 @@ import { DateTime, Effect, Layer, Semaphore, ServiceMap } from "effect" -import { runPromiseInstance } from "@/effect/runtime" +import { makeRuntimeInstance } from "@/effect/runtime" import { Flag } from "@/flag/flag" import type { SessionID } from "@/session/schema" import { Filesystem } from "../util/filesystem" @@ -92,19 +92,21 @@ export namespace FileTime { }), ) + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) + export function read(sessionID: SessionID, file: string) { - return runPromiseInstance(Service.use((s) => s.read(sessionID, file))) + return runPromise((s) => s.read(sessionID, file)) } export function get(sessionID: SessionID, file: string) { - return runPromiseInstance(Service.use((s) => s.get(sessionID, file))) + return runPromise((s) => s.get(sessionID, file)) } export async function assert(sessionID: SessionID, filepath: string) { - return runPromiseInstance(Service.use((s) => s.assert(sessionID, filepath))) + return runPromise((s) => s.assert(sessionID, filepath)) } export async function withLock(filepath: string, fn: () => Promise): Promise { - return runPromiseInstance(Service.use((s) => s.withLock(filepath, fn))) + return runPromise((s) => s.withLock(filepath, fn)) } } diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 6da8caa08c8..db82e613c86 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,5 +1,4 @@ import { Effect, Layer, ServiceMap } from "effect" -import { runPromiseInstance } from "@/effect/runtime" import { InstanceContext } from "@/effect/instance-context" import path from "path" import { mergeDeep } from "remeda" @@ -11,6 +10,7 @@ import { Instance } from "../project/instance" import { Process } from "../util/process" import { Log } from "../util/log" import * as Formatter from "./formatter" +import { makeRuntimeInstance } from "@/effect/runtime" export namespace Format { const log = Log.create({ service: "format" }) @@ -151,7 +151,9 @@ export namespace Format { }), ) + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) + export async function status() { - return runPromiseInstance(Service.use((s) => s.status())) + return runPromise((s) => s.status()) } } diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 93a8c49b655..f82a75ee333 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,4 +1,3 @@ -import { runPromiseInstance } from "@/effect/runtime" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { Config } from "@/config/config" @@ -14,6 +13,7 @@ import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect" import os from "os" import z from "zod" import { PermissionID } from "./schema" +import { makeRuntimeInstance } from "@/effect/runtime" export namespace PermissionNext { const log = Log.create({ service: "permission" }) @@ -246,6 +246,8 @@ export namespace PermissionNext { }), ) + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) + function expand(pattern: string): string { if (pattern.startsWith("~/")) return os.homedir() + pattern.slice(1) if (pattern === "~") return os.homedir() @@ -272,12 +274,12 @@ export namespace PermissionNext { return rulesets.flat() } - export const ask = fn(AskInput, async (input) => runPromiseInstance(Service.use((svc) => svc.ask(input)))) + export const ask = fn(AskInput, async (input) => runPromise((svc) => svc.ask(input))) - export const reply = fn(ReplyInput, async (input) => runPromiseInstance(Service.use((svc) => svc.reply(input)))) + export const reply = fn(ReplyInput, async (input) => runPromise((svc) => svc.reply(input))) export async function list() { - return runPromiseInstance(Service.use((svc) => svc.list())) + return runPromise((svc) => svc.list()) } const EDIT_TOOLS = ["edit", "write", "apply_patch", "multiedit"] diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 6075540161b..680ff5b788f 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,11 +1,11 @@ import { GlobalBus } from "@/bus/global" -import { disposeInstance } from "@/effect/instance-registry" import { Filesystem } from "@/util/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util/log" import { Context } from "../util/context" import { Project } from "./project" import { State } from "./state" +import { disposeInstanceRuntimes } from "@/effect/runtime" interface Context { directory: string @@ -119,7 +119,7 @@ export const Instance = { async reload(input: { directory: string; init?: () => Promise; project?: Project.Info; worktree?: string }) { const directory = Filesystem.resolve(input.directory) Log.Default.info("reloading instance", { directory }) - await Promise.all([State.dispose(directory), disposeInstance(directory)]) + await Promise.all([State.dispose(directory), disposeInstanceRuntimes(directory)]) cache.delete(directory) const next = track(directory, boot({ ...input, directory })) emit(directory) @@ -128,7 +128,7 @@ export const Instance = { async dispose() { const directory = Instance.directory Log.Default.info("disposing instance", { directory }) - await Promise.all([State.dispose(directory), disposeInstance(directory)]) + await Promise.all([State.dispose(directory), disposeInstanceRuntimes(directory)]) cache.delete(directory) emit(directory) }, diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 9e85571c497..473547a6112 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -1,4 +1,5 @@ import { Effect, Layer, ServiceMap } from "effect" +import { makeRuntimeInstance } from "@/effect/runtime" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceContext } from "@/effect/instance-context" @@ -80,4 +81,6 @@ export namespace Vcs { }) }), ) + + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) } diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 5204b5fb8d3..886495ae015 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,11 +1,11 @@ import type { AuthOuathResult } from "@opencode-ai/plugin" import { NamedError } from "@opencode-ai/util/error" import * as Auth from "@/auth/effect" -import { runPromiseInstance } from "@/effect/runtime" import { fn } from "@/util/fn" import { ProviderID } from "./schema" -import { Array as Arr, Effect, Layer, Record, Result, ServiceMap, Struct } from "effect" +import { Array as Arr, Effect, Layer, Record, Result, ServiceMap } from "effect" import z from "zod" +import { makeRuntimeGlobal } from "@/effect/runtime" export namespace ProviderAuth { export const Method = z @@ -215,8 +215,10 @@ export namespace ProviderAuth { export const defaultLayer = layer.pipe(Layer.provide(Auth.AuthEffect.layer)) + export const { runtime, runSync, runPromise } = makeRuntimeGlobal(Service, defaultLayer) + export async function methods() { - return runPromiseInstance(Service.use((svc) => svc.methods())) + return runPromise((svc) => svc.methods()) } export const authorize = fn( @@ -225,7 +227,7 @@ export namespace ProviderAuth { method: z.number(), inputs: z.record(z.string(), z.string()).optional(), }), - async (input): Promise => runPromiseInstance(Service.use((svc) => svc.authorize(input))), + async (input): Promise => runPromise((svc) => svc.authorize(input)), ) export const callback = fn( @@ -234,6 +236,6 @@ export namespace ProviderAuth { method: z.number(), code: z.string().optional(), }), - async (input) => runPromiseInstance(Service.use((svc) => svc.callback(input))), + async (input) => runPromise((svc) => svc.callback(input)), ) } diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 551c5139999..514a4eab7e7 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,5 +1,5 @@ import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect" -import { runPromiseInstance } from "@/effect/runtime" +import { makeRuntimeInstance } from "@/effect/runtime" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { SessionID, MessageID } from "@/session/schema" @@ -171,23 +171,25 @@ export namespace Question { }), ) + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, Layer.fresh(layer)) + export async function ask(input: { sessionID: SessionID questions: Info[] tool?: { messageID: MessageID; callID: string } }): Promise { - return runPromiseInstance(Service.use((svc) => svc.ask(input))) + return runPromise((svc) => svc.ask(input)) } export async function reply(input: { requestID: QuestionID; answers: Answer[] }): Promise { - return runPromiseInstance(Service.use((svc) => svc.reply(input))) + return runPromise((svc) => svc.reply(input)) } export async function reject(requestID: QuestionID): Promise { - return runPromiseInstance(Service.use((svc) => svc.reject(requestID))) + return runPromise((svc) => svc.reject(requestID)) } export async function list(): Promise { - return runPromiseInstance(Service.use((svc) => svc.list())) + return runPromise((svc) => svc.list()) } } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a68becb1fba..d156e99b282 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -12,7 +12,6 @@ import { Format } from "../format" import { TuiRoutes } from "./routes/tui" import { Instance } from "../project/instance" import { Vcs } from "../project/vcs" -import { runPromiseInstance } from "@/effect/runtime" import { Agent } from "../agent/agent" import { Skill } from "../skill/skill" import { Auth } from "../auth" @@ -331,7 +330,7 @@ export namespace Server { }, }), async (c) => { - const branch = await runPromiseInstance(Vcs.Service.use((s) => s.branch())) + const branch = await Vcs.runPromise((s) => s.branch()) return c.json({ branch, }) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 5339691a01b..fdd2ee66a77 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -7,7 +7,6 @@ import { NamedError } from "@opencode-ai/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { InstanceContext } from "@/effect/instance-context" -import { runPromiseInstance } from "@/effect/runtime" import { Flag } from "@/flag/flag" import { Global } from "@/global" import { PermissionNext } from "@/permission" @@ -17,6 +16,7 @@ import { ConfigMarkdown } from "../config/markdown" import { Glob } from "../util/glob" import { Log } from "../util/log" import { Discovery } from "./discovery" +import { makeRuntimeInstance } from "@/effect/runtime" export namespace Skill { const log = Log.create({ service: "skill" }) @@ -214,23 +214,26 @@ export namespace Skill { ) export const defaultLayer: Layer.Layer = layer.pipe( + Layer.fresh, Layer.provide(Discovery.defaultLayer), ) + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, defaultLayer) + export async function get(name: string) { - return runPromiseInstance(Service.use((skill) => skill.get(name))) + return runPromise((skill) => skill.get(name)) } export async function all() { - return runPromiseInstance(Service.use((skill) => skill.all())) + return runPromise((skill) => skill.all()) } export async function dirs() { - return runPromiseInstance(Service.use((skill) => skill.dirs())) + return runPromise((skill) => skill.dirs()) } export async function available(agent?: Agent.Info) { - return runPromiseInstance(Service.use((skill) => skill.available(agent))) + return runPromise((skill) => skill.available(agent)) } export function fmt(list: Info[], opts: { verbose: boolean }) { diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 887bce33416..91417d2f9a0 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -4,11 +4,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import path from "path" import z from "zod" import { InstanceContext } from "@/effect/instance-context" -import { runPromiseInstance } from "@/effect/runtime" import { AppFileSystem } from "@/filesystem" import { Config } from "../config/config" import { Global } from "../global" import { Log } from "../util/log" +import { makeRuntimeInstance } from "@/effect/runtime" export namespace Snapshot { export const Patch = z.object({ @@ -32,31 +32,31 @@ export namespace Snapshot { export type FileDiff = z.infer export async function cleanup() { - return runPromiseInstance(Service.use((svc) => svc.cleanup())) + return runPromise((svc) => svc.cleanup()) } export async function track() { - return runPromiseInstance(Service.use((svc) => svc.track())) + return runPromise((svc) => svc.track()) } export async function patch(hash: string) { - return runPromiseInstance(Service.use((svc) => svc.patch(hash))) + return runPromise((svc) => svc.patch(hash)) } export async function restore(snapshot: string) { - return runPromiseInstance(Service.use((svc) => svc.restore(snapshot))) + return runPromise((svc) => svc.restore(snapshot)) } export async function revert(patches: Patch[]) { - return runPromiseInstance(Service.use((svc) => svc.revert(patches))) + return runPromise((svc) => svc.revert(patches)) } export async function diff(hash: string) { - return runPromiseInstance(Service.use((svc) => svc.diff(hash))) + return runPromise((svc) => svc.diff(hash)) } export async function diffFull(from: string, to: string) { - return runPromiseInstance(Service.use((svc) => svc.diffFull(from, to))) + return runPromise((svc) => svc.diffFull(from, to)) } const log = Log.create({ service: "snapshot" }) @@ -341,9 +341,12 @@ export namespace Snapshot { ) export const defaultLayer = layer.pipe( + Layer.fresh, Layer.provide(NodeChildProcessSpawner.layer), Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodeFileSystem.layer), // needed by NodeChildProcessSpawner Layer.provide(NodePath.layer), ) + + export const { runtime, runSync, runPromise } = makeRuntimeInstance(Service, defaultLayer) } diff --git a/packages/opencode/src/tool/truncate-effect.ts b/packages/opencode/src/tool/truncate-effect.ts index 4431c18f839..21c6b74b9b0 100644 --- a/packages/opencode/src/tool/truncate-effect.ts +++ b/packages/opencode/src/tool/truncate-effect.ts @@ -8,6 +8,7 @@ import { Identifier } from "../id/id" import { Log } from "../util/log" import { ToolID } from "./schema" import { TRUNCATION_DIR } from "./truncation-dir" +import { makeRuntimeGlobal } from "@/effect/runtime" export namespace TruncateEffect { const log = Log.create({ service: "truncation" }) @@ -134,4 +135,6 @@ export namespace TruncateEffect { ) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer)) + + export const { runtime, runSync, runPromise } = makeRuntimeGlobal(Service, defaultLayer) } diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index 159b2d1d5b7..c1b9f6b747f 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -1,5 +1,4 @@ import type { Agent } from "../agent/agent" -import { runtime } from "@/effect/runtime" import { TruncateEffect as S } from "./truncate-effect" export namespace Truncate { @@ -13,6 +12,6 @@ export namespace Truncate { export type Options = S.Options export async function output(text: string, options: Options = {}, agent?: Agent.Info): Promise { - return runtime.runPromise(S.Service.use((s) => s.output(text, options, agent))) + return S.runPromise((s) => s.output(text, options, agent)) } } diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 2a6b6e0bafa..70171b4785d 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -2,8 +2,6 @@ import { afterEach, test, expect } from "bun:test" import os from "os" import { Effect } from "effect" import { Bus } from "../../src/bus" -import { runtime } from "../../src/effect/runtime" -import { Instances } from "../../src/effect/instances" import { PermissionNext } from "../../src/permission" import { PermissionNext as S } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" @@ -1004,8 +1002,8 @@ test("ask - abort should clear pending request", async () => { directory: tmp.path, fn: async () => { const ctl = new AbortController() - const ask = runtime.runPromise( - S.Service.use((svc) => + const ask = PermissionNext.runPromise( + (svc) => svc.ask({ sessionID: SessionID.make("session_test"), permission: "bash", @@ -1014,7 +1012,6 @@ test("ask - abort should clear pending request", async () => { always: [], ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], }), - ).pipe(Effect.provide(Instances.get(Instance.directory))), { signal: ctl.signal }, )