Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/cross-spawn-spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import * as NodeChildProcess from "node:child_process"
import { PassThrough } from "node:stream"
import launch from "cross-spawn"
import { LayerNode } from "./effect/layer-node"
import { filesystem, path } from "./effect/layer-node-platform"

const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))

Expand Down Expand Up @@ -501,5 +503,6 @@ export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSyste
)

export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
export const node = LayerNode.make(layer, [filesystem, path])

export * as CrossSpawnSpawner from "./cross-spawn-spawner"
3 changes: 3 additions & 0 deletions packages/core/src/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Flag } from "../flag/flag"
import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version"
import { LayerNode } from "../effect/layer-node"

const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
type DatabaseShape = Effect.Success<typeof makeDatabase>
Expand Down Expand Up @@ -58,3 +59,5 @@ export const defaultLayer = Layer.unwrap(
return layerFromPath(path())
}),
).pipe(Layer.provide(Global.defaultLayer))

export const node = LayerNode.make(layerFromPath(path()), [])
12 changes: 12 additions & 0 deletions packages/core/src/effect/layer-node-platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
import { FetchHttpClient } from "effect/unstable/http"
import { LayerNode } from "./layer-node"

export const filesystem = LayerNode.make(NodeFileSystem.layer, [])
export const path = LayerNode.make(NodePath.layer, [])
export const httpClient = LayerNode.make(FetchHttpClient.layer, [])
export const requestExecutor = LayerNode.make(RequestExecutor.layer, [httpClient])
export const llmClient = LayerNode.make(LLMClient.layer, [requestExecutor])

export * as LayerNodePlatform from "./layer-node-platform"
95 changes: 95 additions & 0 deletions packages/core/src/effect/layer-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Layer } from "effect"

type RuntimeLayer = Layer.Layer<never, unknown, unknown>
type AnyNode = Node<unknown, unknown>
type NodeList = readonly [] | readonly [AnyNode, ...AnyNode[]]
type Output<Item> = [Item] extends [never] ? never : Item extends Node<infer A, unknown> ? A : never
type Error<Item> = [Item] extends [never] ? never : Item extends Node<unknown, infer E> ? E : never
type Missing<Required, Dependencies extends NodeList> = Exclude<Required, Output<Dependencies[number]>>
type CheckDependencies<Implementation extends Layer.Any, Dependencies extends NodeList> = [
Missing<Layer.Services<Implementation>, Dependencies>,
] extends [never]
? unknown
: { readonly "Missing dependencies": Missing<Layer.Services<Implementation>, Dependencies> }
declare const $OutputType: unique symbol
declare const $ErrorType: unique symbol

export type Node<A, E = never> = {
readonly kind: "layer" | "group"
readonly implementation?: Layer.Any
readonly dependencies: readonly AnyNode[]
readonly [$OutputType]?: () => A
readonly [$ErrorType]?: () => E
}

export function make<const Implementation extends Layer.Any, const Items extends NodeList>(
implementation: Implementation,
dependencies: Items & CheckDependencies<Implementation, NoInfer<Items>>,
): Node<Layer.Success<Implementation>, Layer.Error<Implementation> | Error<Items[number]>> {
return { kind: "layer", implementation: implementation as Layer.Any, dependencies }
}

export function group<const Items extends NodeList>(
dependencies: Items,
): Node<Output<Items[number]>, Error<Items[number]>> {
return { kind: "group", dependencies }
}

export type Replacement<A = unknown> = {
readonly source: Node<A, unknown>
readonly replacement: Node<A, unknown>
}

type CheckReplacementErrors<SourceError, ReplacementError> = [Exclude<ReplacementError, SourceError>] extends [never]
? unknown
: { readonly "New replacement errors": Exclude<ReplacementError, SourceError> }

export function replace<A, E, E2>(
source: Node<A, E>,
replacement: Node<NoInfer<A>, E2> & CheckReplacementErrors<E, NoInfer<E2>>,
): Replacement<A> {
return { source, replacement }
}

export function buildLayer<A, E>(node: Node<A, E>, options?: { readonly replacements?: readonly Replacement[] }) {
const replacements = new Map(options?.replacements?.map((item) => [item.source, item.replacement]))
const cache = new Map<AnyNode, RuntimeLayer>()
const visiting = new Set<AnyNode>()
const stack: AnyNode[] = []
const ids = new Map<AnyNode, number>()

const visit = (input: AnyNode): RuntimeLayer => {
const node = replacements.get(input) ?? input
const cached = cache.get(node)
if (cached) return cached
if (visiting.has(node)) {
const start = stack.indexOf(node)
const cycle = [...stack.slice(start), node].map((item) => `${item.kind}#${ids.get(item)}`).join(" -> ")
throw new Error(`Cycle detected in app graph: ${cycle}`)
}
if (!ids.has(node)) ids.set(node, ids.size + 1)
visiting.add(node)
stack.push(node)
try {
const dependencies = node.dependencies.map(visit)
const nonEmpty = dependencies as [RuntimeLayer, ...RuntimeLayer[]]
const result =
node.kind === "group"
? dependencies.length === 0
? Layer.empty
: Layer.mergeAll(...nonEmpty)
: dependencies.length === 0
? (node.implementation as RuntimeLayer)
: Layer.provide(node.implementation as RuntimeLayer, nonEmpty)
cache.set(node, result)
return result
} finally {
stack.pop()
visiting.delete(node)
}
}

return visit(node) as unknown as Layer.Layer<A, E, never>
}

export * as LayerNode from "./layer-node"
2 changes: 2 additions & 0 deletions packages/core/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location"
import { externalID, type ExternalID, NonNegativeInt, withStatics } from "./schema"
import { Identifier } from "./util/identifier"
import { LayerNode } from "./effect/layer-node"
import { isDeepStrictEqual } from "node:util"

export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
Expand Down Expand Up @@ -674,5 +675,6 @@ export const layerWith = (options?: LayerOptions) =>
)

export const layer = layerWith()
export const node = LayerNode.make(layer, [Database.node])

export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
3 changes: 3 additions & 0 deletions packages/core/src/filesystem/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { CrossSpawnSpawner } from "../cross-spawn-spawner"
import { Global } from "../global"
import { NonNegativeInt } from "../schema"
import { which } from "../util/which"
import { LayerNode } from "../effect/layer-node"
import { httpClient } from "../effect/layer-node-platform"

const VERSION = "15.1.0"
const PLATFORM = {
Expand Down Expand Up @@ -480,5 +482,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
)
export const node = LayerNode.make(layer, [FSUtil.node, CrossSpawnSpawner.node, httpClient])

export * as Ripgrep from "./ripgrep"
2 changes: 2 additions & 0 deletions packages/core/src/filesystem/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { serviceUse } from "../effect/service-use"
import { makeRuntime } from "../effect/runtime"
import { Fff } from "#fff"
import { Ripgrep } from "./ripgrep"
import { LayerNode } from "../effect/layer-node"

const root = path.join(Global.Path.cache, "fff")

Expand Down Expand Up @@ -520,6 +521,7 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
)
export const node = LayerNode.make(layer, [FSUtil.node, Ripgrep.node])

const { runPromise } = makeRuntime(Service, defaultLayer)

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/fs-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { Glob } from "./util/glob"
import { serviceUse } from "./effect/service-use"
import { LayerNode } from "./effect/layer-node"
import { filesystem } from "./effect/layer-node-platform"

export namespace FSUtil {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
Expand Down Expand Up @@ -194,6 +196,7 @@ export namespace FSUtil {
)

export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
export const node = LayerNode.make(layer, [filesystem])

// Pure helpers that don't need Effect (path manipulation, sync operations)
export function mimeType(p: string): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChildProcess } from "effect/unstable/process"
import { AbsolutePath } from "./schema"
import { FSUtil } from "./fs-util"
import { AppProcess } from "./process"
import { LayerNode } from "./effect/layer-node"

export interface Repo {
/**
Expand Down Expand Up @@ -400,6 +401,7 @@ export const layer = Layer.effect(
)

export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node])

export interface Result {
readonly exitCode: number
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import os from "os"
import { Context, Effect, Layer } from "effect"
import { Flock } from "./util/flock"
import { Flag } from "./flag/flag"
import { LayerNode } from "./effect/layer-node"

const app = "opencode"
const data = path.join(xdgData!, app)
Expand Down Expand Up @@ -76,6 +77,7 @@ export const layer = Layer.effect(
)

export const defaultLayer = layer
export const node = LayerNode.make(layer, [])

export const layerWith = (input: Partial<Interface>) =>
Layer.effect(
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/models-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Hash } from "./util/hash"
import { FSUtil } from "./fs-util"
import { InstallationChannel, InstallationVersion } from "./installation/version"
import { EventV2 } from "./event"
import { LayerNode } from "./effect/layer-node"
import { httpClient } from "./effect/layer-node-platform"

export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
Expand Down Expand Up @@ -246,5 +248,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(EventV2.defaultLayer),
)
export const node = LayerNode.make(layer, [FSUtil.node, EventV2.node, httpClient])

export * as ModelsDev from "./models-dev"
3 changes: 3 additions & 0 deletions packages/core/src/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { NodeFileSystem } from "@effect/platform-node"
import { FSUtil } from "./fs-util"
import { Global } from "./global"
import { EffectFlock } from "./util/effect-flock"
import { LayerNode } from "./effect/layer-node"
import { filesystem } from "./effect/layer-node-platform"
import { makeRuntime } from "./effect/runtime"
import { NpmConfig } from "./npm-config"

Expand Down Expand Up @@ -250,6 +252,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(Global.layer),
Layer.provide(NodeFileSystem.layer),
)
export const node = LayerNode.make(layer, [FSUtil.node, Global.node, filesystem, EffectFlock.node])

const { runPromise } = makeRuntime(Service, defaultLayer)

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { PlatformError } from "effect/PlatformError"
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "./cross-spawn-spawner"
import { LayerNode } from "./effect/layer-node"

export class AppProcessError extends Schema.TaggedErrorClass<AppProcessError>()("AppProcessError", {
command: Schema.String,
Expand Down Expand Up @@ -230,5 +231,6 @@ export const layer = Layer.effect(
)

export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
export const node = LayerNode.make(layer, [CrossSpawnSpawner.node])

export * as AppProcess from "./process"
2 changes: 2 additions & 0 deletions packages/core/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AbsolutePath, withStatics } from "./schema"
import { FSUtil } from "./fs-util"
import { Database } from "./database/database"
import { Git } from "./git"
import { LayerNode } from "./effect/layer-node"
import { Hash } from "./util/hash"
import { ProjectDirectoryTable } from "./project/sql"

Expand Down Expand Up @@ -159,3 +160,4 @@ export const defaultLayer = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Git.defaultLayer),
)
export const node = LayerNode.make(layer, [Database.node, FSUtil.node, Git.node])
2 changes: 2 additions & 0 deletions packages/core/src/project/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FSUtil } from "../fs-util"
import { Git } from "../git"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { LayerNode } from "../effect/layer-node"
import { Project } from "../project"
import { ProjectDirectoryTable } from "./sql"
import { makeStrategies } from "./copy-strategies"
Expand Down Expand Up @@ -275,3 +276,4 @@ export const defaultLayer = layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(EventV2.defaultLayer),
)
export const node = LayerNode.make(layer, [FSUtil.node, Git.node, EventV2.node, Database.node])
2 changes: 2 additions & 0 deletions packages/core/src/pty/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { WorkspaceV2 } from "../workspace"
import { PositiveInt } from "../schema"
import { PtyID } from "./schema"
import { Cache, Context, Duration, Effect, Layer, Schema } from "effect"
import { LayerNode } from "../effect/layer-node"

const DEFAULT_TTL = Duration.seconds(60)
const CAPACITY = 10_000
Expand Down Expand Up @@ -56,3 +57,4 @@ export const make = (ttl: Duration.Input = DEFAULT_TTL) =>
export const layer = Layer.effect(Service, make())

export const defaultLayer = layer
export const node = LayerNode.make(layer, [])
2 changes: 2 additions & 0 deletions packages/core/src/session/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { and, desc, eq, sql } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { LayerNode } from "../effect/layer-node"
import { SessionEvent } from "./event"
import { SessionV1 } from "../v1/session"
import { WorkspaceTable } from "../control-plane/workspace.sql"
Expand Down Expand Up @@ -447,3 +448,4 @@ export const layer = Layer.effectDiscard(
)

export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(Database.defaultLayer))
export const node = LayerNode.make(layer, [EventV2.node, Database.node])
2 changes: 2 additions & 0 deletions packages/core/src/util/effect-flock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { FileSystem, Scope } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { FSUtil } from "../fs-util"
import { Global } from "../global"
import { LayerNode } from "../effect/layer-node"
import { Hash } from "./hash"

export namespace EffectFlock {
Expand Down Expand Up @@ -280,4 +281,5 @@ export namespace EffectFlock {
)

export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.layer))
export const node = LayerNode.make(layer, [Global.node, FSUtil.node])
}
4 changes: 4 additions & 0 deletions packages/opencode/src/account/account.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import {
Expand Down Expand Up @@ -456,4 +458,6 @@ export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient

export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(FetchHttpClient.layer))

export const node = LayerNode.make(layer, [AccountRepo.node, httpClient])

export * as Account from "./account"
3 changes: 3 additions & 0 deletions packages/opencode/src/account/repo.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { eq } from "drizzle-orm"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Effect, Layer, Option, Schema, Context } from "effect"
Expand Down Expand Up @@ -167,4 +168,6 @@ export const layer = Layer.effect(

export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))

export const node = LayerNode.make(layer, [Database.node])

export * as AccountRepo from "./repo"
3 changes: 3 additions & 0 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Config } from "@/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
Expand Down Expand Up @@ -430,4 +431,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(Skill.defaultLayer),
)

export const node = LayerNode.make(layer, [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node])

export * as Agent from "./agent"
3 changes: 3 additions & 0 deletions packages/opencode/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
Expand Down Expand Up @@ -93,4 +94,6 @@ export const layer = Layer.effect(

export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer))

export const node = LayerNode.make(layer, [FSUtil.node])

export * as Auth from "."
Loading
Loading