feat(opencode): add typed application layer graph (#31531)

This commit is contained in:
James Long 2026-06-09 15:31:31 -04:00 committed by GitHub
commit 07e5ea9367
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 759 additions and 0 deletions

View file

@ -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 {
@ -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"

View file

@ -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"
@ -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"

View file

@ -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"
@ -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"

View file

@ -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"
@ -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 "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { BackgroundJob as CoreBackgroundJob } from "@opencode-ai/core/background-job"
import { InstanceState } from "@/effect/instance-state"
import { Effect, Layer } from "effect"
@ -33,4 +34,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer
export const node = LayerNode.make(layer, [])
export * as BackgroundJob from "./job"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state"
import { EffectBridge } from "@/effect/bridge"
import type { InstanceContext } from "@/project/instance-context"
@ -178,4 +179,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(Skill.defaultLayer),
)
export const node = LayerNode.make(layer, [Config.node, MCP.node, Skill.node])
export * as Command from "."

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import path from "path"
import { pathToFileURL } from "url"
@ -669,4 +671,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(FetchHttpClient.layer),
)
export const node = LayerNode.make(layer, [FSUtil.node, Auth.node, Account.node, Env.node, Npm.node, httpClient])
export * as Config from "./config"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http"
@ -972,4 +974,16 @@ function route(url: string | URL, path: string) {
return next
}
export const node = LayerNode.make(layer, [
Auth.node,
Session.node,
SessionPrompt.node,
httpClient,
EventV2Bridge.node,
Vcs.node,
RuntimeFlags.node,
FSUtil.node,
Database.node,
])
export * as Workspace from "./workspace"

View file

@ -73,4 +73,7 @@ export const layer = (overrides: Partial<Info> = {}) =>
export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
export const node = LayerNode.make(defaultLayer, [])
export * as RuntimeFlags from "./runtime-flags"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
@ -37,4 +38,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer
export const node = LayerNode.make(layer, [])
export * as Env from "."

View file

@ -1,5 +1,6 @@
// Opencode publish boundary for core events. Attach routed instance location
// so direct EventV2 consumers can isolate directory/workspace streams.
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { GlobalBus } from "@/bus/global"
import { EventV2 } from "@opencode-ai/core/event"
@ -73,4 +74,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer))
export const node = LayerNode.make(layer, [EventV2.node])
export * as EventV2Bridge from "./event-v2-bridge"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { ChildProcess } from "effect/unstable/process"
@ -199,4 +200,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make(layer, [Config.node, AppProcess.node, RuntimeFlags.node])
export * as Format from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppProcess } from "@opencode-ai/core/process"
import { Effect, Layer, Context, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process"
@ -344,4 +345,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make(layer, [AppProcess.node])
export * as Git from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Config } from "@/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { MessageV2 } from "@/session/message-v2"
@ -168,4 +169,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
export const node = LayerNode.make(layer, [Config.node])
export * as Image from "./image"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
@ -343,4 +345,6 @@ export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((
export const method = () => runPromise((s) => s.method())
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
export const node = LayerNode.make(layer, [httpClient, AppProcess.node])
export * as Installation from "."

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import * as LSPClient from "./client"
@ -504,4 +506,6 @@ export const defaultLayer = layer.pipe(
export * as Diagnostic from "./diagnostic"
export const node = LayerNode.make(layer, [Config.node, RuntimeFlags.node, FSUtil.node, EventV2Bridge.node])
export * as LSP from "./lsp"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import path from "path"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Global } from "@opencode-ai/core/global"
@ -168,4 +169,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make(layer, [FSUtil.node, EffectFlock.node])
export * as McpAuth from "./auth"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
@ -1003,4 +1004,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(FSUtil.defaultLayer),
)
export const node = LayerNode.make(layer, [CrossSpawnSpawner.node, McpAuth.node, EventV2Bridge.node, Config.node])
export * as MCP from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { InstanceState } from "@/effect/instance-state"
import { Wildcard } from "@opencode-ai/core/util/wildcard"
@ -224,4 +225,6 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<st
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make(layer, [EventV2Bridge.node])
export * as Permission from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import type {
Hooks,
PluginInput,
@ -307,4 +308,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make(layer, [EventV2Bridge.node, Config.node, RuntimeFlags.node])
export * as Plugin from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Plugin } from "../plugin"
import { Format } from "../format"
import { LSP } from "@/lsp/lsp"
@ -74,4 +75,16 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
]),
)
export const node = LayerNode.make(layer, [
Config.node,
Format.node,
LSP.node,
Plugin.node,
Project.node,
Search.node,
ShareNext.node,
Snapshot.node,
Vcs.node,
])
export * as InstanceBootstrap from "./bootstrap"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context"
@ -7,6 +8,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service"
import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap"
import * as Project from "./project"
export interface LoadInput {
@ -202,4 +204,6 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
export const node = LayerNode.make(layer, [Project.node, InstanceBootstrapGraph.node])
export * as InstanceStore from "./instance-store"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { and, eq, sql } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql"
@ -514,4 +515,15 @@ export const defaultLayer = layer.pipe(
export const use = serviceUse(Service)
export const node = LayerNode.make(layer, [
FSUtil.node,
AppProcess.node,
CrossSpawnSpawner.node,
ProjectV2.node,
ProjectCopy.node,
EventV2Bridge.node,
RuntimeFlags.node,
Database.node,
])
export * as Project from "./project"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema, Stream, Scope } from "effect"
import { formatPatch, structuredPatch } from "diff"
import { InstanceState } from "@/effect/instance-state"
@ -425,4 +426,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make(layer, [Git.node, EventV2Bridge.node])
export * as Vcs from "./vcs"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Auth } from "@/auth"
@ -227,4 +228,6 @@ export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
)
export const node = LayerNode.make(layer, [Auth.node, Plugin.node])
export * as ProviderAuth from "./auth"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import os from "os"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import fuzzysort from "fuzzysort"
@ -1948,4 +1949,14 @@ export function parseModel(model: string) {
}
}
export const node = LayerNode.make(layer, [
FSUtil.node,
Config.node,
Auth.node,
Env.node,
Plugin.node,
ModelsDev.node,
RuntimeFlags.node,
])
export * as Provider from "./provider"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Deferred, Effect, Layer, Schema, Context } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { SessionID, MessageID } from "@/session/schema"
@ -223,4 +224,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make(layer, [EventV2Bridge.node])
export * as Question from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Session } from "./session"
@ -605,4 +606,15 @@ export const defaultLayer = Layer.suspend(() =>
),
)
export const node = LayerNode.make(layer, [
Config.node,
Session.node,
Agent.node,
Plugin.node,
SessionProcessor.node,
Provider.node,
EventV2Bridge.node,
RuntimeFlags.node,
])
export * as SessionCompaction from "./compaction"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import path from "path"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Layer, Context } from "effect"
@ -234,4 +236,6 @@ export function loaded(messages: SessionV1.WithParts[]) {
return extract(messages)
}
export const node = LayerNode.make(layer, [Config.node, FSUtil.node, Global.node, RuntimeFlags.node, httpClient])
export * as Instruction from "./instruction"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { llmClient } from "@opencode-ai/core/effect/layer-node-platform"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Provider } from "@/provider/provider"
import { SessionV1 } from "@opencode-ai/core/v1/session"
@ -399,4 +401,15 @@ export const defaultLayer = Layer.suspend(() =>
export const hasToolCalls = LLMRequestPrep.hasToolCalls
export const node = LayerNode.make(layer, [
Auth.node,
Config.node,
Provider.node,
Plugin.node,
Permission.node,
EventV2Bridge.node,
llmClient,
RuntimeFlags.node,
])
export * as LLM from "./llm"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Image } from "@/image/image"
import { SessionV1 } from "@opencode-ai/core/v1/session"
@ -1064,4 +1065,20 @@ export const defaultLayer = Layer.suspend(() =>
),
)
export const node = LayerNode.make(layer, [
Session.node,
Config.node,
Snapshot.node,
Agent.node,
LLM.node,
Permission.node,
Plugin.node,
SessionSummary.node,
SessionStatus.node,
Image.node,
EventV2Bridge.node,
RuntimeFlags.node,
Database.node,
])
export * as SessionProcessor from "./processor"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import path from "path"
import { SessionV1 } from "@opencode-ai/core/v1/session"
@ -1671,4 +1672,33 @@ const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
const placeholderRegex = /\$(\d+)/g
const quoteTrimRegex = /^["']|["']$/g
export const node = LayerNode.make(layer, [
SessionStatus.node,
Session.node,
Agent.node,
Provider.node,
SessionProcessor.node,
SessionCompaction.node,
Plugin.node,
Command.node,
Config.node,
Permission.node,
FSUtil.node,
MCP.node,
LSP.node,
ToolRegistry.node,
Truncate.node,
Image.node,
CrossSpawnSpawner.node,
Instruction.node,
SessionRunState.node,
SessionRevert.node,
SessionSummary.node,
SystemPrompt.node,
LLM.node,
EventV2Bridge.node,
RuntimeFlags.node,
Database.node,
])
export * as SessionPrompt from "./prompt"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
@ -147,4 +148,13 @@ export const defaultLayer = Layer.suspend(() =>
),
)
export const node = LayerNode.make(layer, [
Session.node,
Snapshot.node,
Storage.node,
EventV2Bridge.node,
SessionSummary.node,
SessionRunState.node,
])
export * as SessionRevert from "./revert"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Runner } from "@/effect/runner"
@ -150,4 +151,6 @@ function busyError(sessionID: SessionID) {
return new Session.BusyError({ sessionID })
}
export const node = LayerNode.make(layer, [BackgroundJob.node, SessionStatus.node])
export * as SessionRunState from "./run-state"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Slug } from "@opencode-ai/core/util/slug"
import { SessionV1 } from "@opencode-ai/core/v1/session"
@ -1113,4 +1114,6 @@ export function* listGlobal(input?: {
}
}
export const node = LayerNode.make(layer, [BackgroundJob.node, RuntimeFlags.node, Database.node, EventV2Bridge.node])
export * as Session from "./session"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceState } from "@/effect/instance-state"
import { SessionID } from "./schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
@ -91,4 +92,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make(layer, [EventV2Bridge.node])
export * as SessionStatus from "./status"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Context, Schema } from "effect"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
@ -159,4 +160,6 @@ export const DiffInput = Schema.Struct({
})
export type DiffInput = Schema.Schema.Type<typeof DiffInput>
export const node = LayerNode.make(layer, [Session.node, Snapshot.node, EventV2Bridge.node, Config.node])
export * as SessionSummary from "./summary"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Effect, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
@ -81,4 +82,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
export const node = LayerNode.make(layer, [Skill.node])
export * as SystemPrompt from "./system"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionID } from "./schema"
import { Effect, Layer, Context, Schema } from "effect"
import { Database } from "@opencode-ai/core/database/database"
@ -84,4 +85,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer), Layer.provide(Database.defaultLayer))
export const node = LayerNode.make(layer, [EventV2Bridge.node, Database.node])
export * as Todo from "./todo"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Session } from "@/session/session"
import { SessionID } from "@/session/schema"
import { Effect, Layer, Scope, Context } from "effect"
@ -55,4 +56,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make(layer, [Config.node, Session.node, ShareNext.node, RuntimeFlags.node])
export * as SessionShare from "./session"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import type * as SDK from "@opencode-ai/sdk/v2"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
@ -370,4 +372,14 @@ export const defaultLayer = layer.pipe(
Layer.provide(Session.defaultLayer),
)
export const node = LayerNode.make(layer, [
Account.node,
EventV2Bridge.node,
Config.node,
Database.node,
httpClient,
Provider.node,
Session.node,
])
export * as ShareNext from "./share-next"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient, path } from "@opencode-ai/core/effect/layer-node-platform"
import { NodePath } from "@effect/platform-node"
import { Effect, Layer, Path, Schema, Context } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
@ -102,4 +104,6 @@ export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide(NodePath.layer),
)
export const node = LayerNode.make(layer, [FSUtil.node, path, httpClient])
export * as Discovery from "./discovery"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer, Context, Schema } from "effect"
@ -353,4 +354,13 @@ export function fmt(list: Info[], opts: { verbose: boolean }) {
].join("\n")
}
export const node = LayerNode.make(layer, [
Discovery.node,
Config.node,
EventV2Bridge.node,
FSUtil.node,
Global.node,
RuntimeFlags.node,
])
export * as Skill from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { formatPatch, structuredPatch } from "diff"
@ -756,4 +757,6 @@ export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
)
export const node = LayerNode.make(layer, [FSUtil.node, AppProcess.node, Config.node])
export * as Snapshot from "."

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import path from "path"
import { Global } from "@opencode-ai/core/global"
import { FSUtil } from "@opencode-ai/core/fs-util"
@ -323,4 +324,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer))
export const node = LayerNode.make(layer, [FSUtil.node, Git.node])
export * as Storage from "./storage"

View file

@ -1,3 +1,6 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { PlanExitTool } from "./plan"
import { Session } from "@/session/session"
import { QuestionTool } from "./question"
@ -437,4 +440,28 @@ function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
export const node = LayerNode.make(layer, [
Config.node,
Plugin.node,
Question.node,
Todo.node,
Agent.node,
Skill.node,
Session.node,
BackgroundJob.node,
Provider.node,
LSP.node,
Instruction.node,
FSUtil.node,
EventV2Bridge.node,
httpClient,
CrossSpawnSpawner.node,
Ripgrep.node,
Search.node,
Format.node,
Truncate.node,
RuntimeFlags.node,
Database.node,
])
export * as ToolRegistry from "./registry"

View file

@ -1,3 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { NodePath } from "@effect/platform-node"
import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
import path from "path"
@ -152,4 +153,6 @@ export const layer = Layer.effect(
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(NodePath.layer))
export const node = LayerNode.make(layer, [FSUtil.node])
export * as Truncate from "./truncate"

View file

@ -1,3 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { path } from "@opencode-ai/core/effect/layer-node-platform"
import { Global } from "@opencode-ai/core/global"
import { InstanceLayer } from "@/project/instance-layer"
import { InstanceStore } from "@/project/instance-store"
@ -639,4 +641,14 @@ export const appLayer = layer.pipe(
export const defaultLayer = appLayer.pipe(Layer.provide(InstanceLayer.layer))
export const node = LayerNode.make(layer, [
FSUtil.node,
path,
AppProcess.node,
Git.node,
Project.node,
InstanceStore.node,
Database.node,
])
export * as Worktree from "."

View file

@ -0,0 +1,101 @@
import { test } from "bun:test"
import { Context, Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
class A extends Context.Service<A, { readonly value: "a" }>()("test/A") {}
class B extends Context.Service<B, { readonly value: "b" }>()("test/B") {}
class C extends Context.Service<C, { readonly value: "c" }>()("test/C") {}
class LayerError {
readonly _tag = "LayerError"
}
class NotFoundError {
readonly _tag = "NotFoundError"
}
class DiskError {
readonly _tag = "DiskError"
}
class NetworkError {
readonly _tag = "NetworkError"
}
const aImplementation = Layer.succeed(A, A.of({ value: "a" }))
const bImplementation = Layer.effect(B, Effect.gen(function* () {
yield* A
return B.of({ value: "b" })
}))
const cImplementation = Layer.effect(C, Effect.gen(function* () {
yield* A
yield* B
return C.of({ value: "c" })
}))
const failingAImplementation = Layer.effect(A, Effect.fail(new LayerError()))
const notFoundAImplementation = Layer.effect(A, Effect.fail(new NotFoundError()))
const diskAImplementation = Layer.effect(A, Effect.fail(new DiskError()))
const networkAImplementation = Layer.effect(A, Effect.fail(new NetworkError()))
const notFoundOrDiskAImplementation = Layer.effect(
A,
Effect.fail(new NotFoundError() as NotFoundError | DiskError),
)
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
type Assert<T extends true> = T
type AProvides = Assert<Equal<Layer.Success<typeof aImplementation>, A>>
type ARequires = Assert<Equal<Layer.Services<typeof aImplementation>, never>>
type BProvides = Assert<Equal<Layer.Success<typeof bImplementation>, B>>
type BRequires = Assert<Equal<Layer.Services<typeof bImplementation>, A>>
type CRequires = Assert<Equal<Layer.Services<typeof cImplementation>, A | B>>
void (0 as unknown as AProvides)
void (0 as unknown as ARequires)
void (0 as unknown as BProvides)
void (0 as unknown as BRequires)
void (0 as unknown as CRequires)
const a = LayerNode.make(aImplementation, [])
const b = LayerNode.make(bImplementation, [a])
const c = LayerNode.make(cImplementation, [a, b])
const failingA = LayerNode.make(failingAImplementation, [])
const bWithFailingA = LayerNode.make(bImplementation, [failingA])
const notFoundA = LayerNode.make(notFoundAImplementation, [])
const diskA = LayerNode.make(diskAImplementation, [])
const networkA = LayerNode.make(networkAImplementation, [])
const notFoundOrDiskA = LayerNode.make(notFoundOrDiskAImplementation, [])
// @ts-expect-error B requires A
LayerNode.make(bImplementation, [])
// @ts-expect-error C requires both A and B
LayerNode.make(cImplementation, [a])
type ANodeProvides = Assert<Equal<typeof a, LayerNode.Node<A, never>>>
type BNodeProvides = Assert<Equal<typeof b, LayerNode.Node<B, never>>>
type CNodeProvides = Assert<Equal<typeof c, LayerNode.Node<C, never>>>
type FailingANodeError = Assert<Equal<typeof failingA, LayerNode.Node<A, LayerError>>>
type DependentNodeError = Assert<Equal<typeof bWithFailingA, LayerNode.Node<B, LayerError>>>
void (0 as unknown as ANodeProvides)
void (0 as unknown as BNodeProvides)
void (0 as unknown as CNodeProvides)
void (0 as unknown as FailingANodeError)
void (0 as unknown as DependentNodeError)
const closed = LayerNode.buildLayer(c)
const closedWithError = LayerNode.buildLayer(bWithFailingA)
type ClosedProvides = Assert<Equal<Layer.Success<typeof closed>, C>>
type ClosedRequires = Assert<Equal<Layer.Services<typeof closed>, never>>
type ClosedError = Assert<Equal<Layer.Error<typeof closedWithError>, LayerError>>
void (0 as unknown as ClosedProvides)
void (0 as unknown as ClosedRequires)
void (0 as unknown as ClosedError)
const replacement = LayerNode.make(Layer.succeed(A, A.of({ value: "a" })), [])
LayerNode.replace(a, replacement)
LayerNode.replace(notFoundOrDiskA, notFoundA)
LayerNode.replace(notFoundOrDiskA, diskA)
// @ts-expect-error An override for A must still provide A
LayerNode.replace(a, b)
// @ts-expect-error A replacement cannot introduce NetworkError
LayerNode.replace(notFoundOrDiskA, networkA)
test("type exploration compiles", () => {})

View file

@ -0,0 +1,208 @@
import { describe, expect, test } from "bun:test"
import { Cause, Context, Effect, Exit, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
const { buildLayer: build, group, replace } = LayerNode
const node = LayerNode.make
class Value extends Context.Service<Value, { readonly value: string }>()("test/Value") {}
class Greeting extends Context.Service<Greeting, { readonly text: string }>()("test/Greeting") {}
const value = LayerNode.make(Layer.succeed(Value, Value.of({ value: "production" })), [])
const greetingImplementation = Layer.effect(
Greeting,
Effect.gen(function* () {
return Greeting.of({ text: `hello ${(yield* Value).value}` })
}),
)
const greeting = LayerNode.make(greetingImplementation, [value])
// @ts-expect-error Greeting requires Value
LayerNode.make(greetingImplementation, [])
describe("app graph", () => {
test("creates any selected dependency layer", async () => {
const result = Effect.gen(function* () {
return (yield* Greeting).text
}).pipe(Effect.provide(build(greeting)))
expect(await Effect.runPromise(result)).toBe("hello production")
})
test("applies overrides before dependency materialization", async () => {
const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
const graph = build(greeting, { replacements: [replace(value, replacement)] })
const result = Effect.gen(function* () {
return (yield* Greeting).text
}).pipe(Effect.provide(graph))
expect(await Effect.runPromise(result)).toBe("hello simulation")
})
test("acquires a shared dependency once", async () => {
class Shared extends Context.Service<Shared, { readonly value: string }>()("test/Shared") {}
class Left extends Context.Service<Left, { readonly value: string }>()("test/Left") {}
class Right extends Context.Service<Right, { readonly value: string }>()("test/Right") {}
let acquisitions = 0
const shared = node(
Layer.effect(
Shared,
Effect.sync(() => {
acquisitions++
return Shared.of({ value: "shared" })
}),
),
[],
)
const left = node(
Layer.effect(
Left,
Effect.gen(function* () {
return Left.of({ value: `${(yield* Shared).value}-left` })
}),
),
[shared],
)
const right = node(
Layer.effect(
Right,
Effect.gen(function* () {
return Right.of({ value: `${(yield* Shared).value}-right` })
}),
),
[shared],
)
const result = Effect.gen(function* () {
return [(yield* Left).value, (yield* Right).value]
}).pipe(Effect.provide(build(group([left, right]))))
expect(await Effect.runPromise(result)).toEqual(["shared-left", "shared-right"])
expect(acquisitions).toBe(1)
})
test("applies a replacement to every transitive consumer", async () => {
class Left extends Context.Service<Left, { readonly value: string }>()("test/ReplacementLeft") {}
class Right extends Context.Service<Right, { readonly value: string }>()("test/ReplacementRight") {}
const left = node(
Layer.effect(
Left,
Effect.gen(function* () {
return Left.of({ value: (yield* Value).value })
}),
),
[value],
)
const right = node(
Layer.effect(
Right,
Effect.gen(function* () {
return Right.of({ value: (yield* Value).value })
}),
),
[value],
)
const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), [])
const graph = build(group([left, right]), { replacements: [replace(value, replacement)] })
const result = Effect.gen(function* () {
return [(yield* Left).value, (yield* Right).value]
}).pipe(Effect.provide(graph))
expect(await Effect.runPromise(result)).toEqual(["simulation", "simulation"])
})
test("propagates layer acquisition errors", async () => {
class AcquisitionError {
readonly _tag = "AcquisitionError"
}
const failing = node(Layer.effect(Value, Effect.fail(new AcquisitionError())), [])
const exit = await Effect.runPromiseExit(Effect.provide(Value, build(failing)))
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(AcquisitionError)
})
test("groups expose every selected service", async () => {
class Count extends Context.Service<Count, { readonly value: number }>()("test/Count") {}
const count = node(Layer.succeed(Count, Count.of({ value: 3 })), [])
const result = Effect.gen(function* () {
return { text: (yield* Value).value, count: (yield* Count).value }
}).pipe(Effect.provide(build(group([value, count]))))
expect(await Effect.runPromise(result)).toEqual({ text: "production", count: 3 })
})
test("builds an empty group", async () => {
expect(await Effect.runPromise(Effect.succeed("ok").pipe(Effect.provide(build(group([])))))).toBe("ok")
})
test("builds replacements with their own dependencies", async () => {
class ReplacementConfig extends Context.Service<ReplacementConfig, { readonly value: string }>()(
"test/ReplacementConfig",
) {}
const replacementConfig = node(
Layer.succeed(ReplacementConfig, ReplacementConfig.of({ value: "replacement" })),
[],
)
const replacement = node(
Layer.effect(
Value,
Effect.gen(function* () {
return Value.of({ value: (yield* ReplacementConfig).value })
}),
),
[replacementConfig],
)
const result = Effect.gen(function* () {
return (yield* Greeting).text
}).pipe(Effect.provide(build(greeting, { replacements: [replace(value, replacement)] })))
expect(await Effect.runPromise(result)).toBe("hello replacement")
})
test("does not acquire unreachable replacements", async () => {
let acquisitions = 0
const unreachable = node(Layer.succeed(Value, Value.of({ value: "unreachable" })), [])
const replacement = node(
Layer.effect(
Value,
Effect.sync(() => {
acquisitions++
return Value.of({ value: "replacement" })
}),
),
[],
)
await Effect.runPromise(Effect.provide(Greeting, build(greeting, { replacements: [replace(unreachable, replacement)] })))
expect(acquisitions).toBe(0)
})
test("rejects a direct cycle", () => {
const cyclic = node(Layer.succeed(Value, Value.of({ value: "cyclic" })), [])
;(cyclic.dependencies as LayerNode.Node<unknown, unknown>[]).push(cyclic)
expect(() => build(cyclic)).toThrow("Cycle detected in app graph: layer#1 -> layer#1")
})
test("rejects an indirect cycle", () => {
const first = node(Layer.succeed(Value, Value.of({ value: "first" })), [])
const second = node(Layer.succeed(Value, Value.of({ value: "second" })), [first])
const third = node(Layer.succeed(Value, Value.of({ value: "third" })), [second])
;(first.dependencies as LayerNode.Node<unknown, unknown>[]).push(third)
expect(() => build(first)).toThrow("Cycle detected in app graph: layer#1 -> layer#2 -> layer#3 -> layer#1")
})
test("rejects a cycle introduced by a replacement", () => {
const replacement = node(Layer.succeed(Value, Value.of({ value: "replacement" })), [])
const consumer = node(greetingImplementation, [value])
;(replacement.dependencies as LayerNode.Node<unknown, unknown>[]).push(consumer)
expect(() => build(consumer, { replacements: [replace(value, replacement)] })).toThrow(
"Cycle detected in app graph: layer#1 -> layer#2 -> layer#1",
)
})
})