refactor(core): support tiered layer nodes (#33937)

This commit is contained in:
James Long 2026-06-25 14:34:17 -04:00 committed by GitHub
commit 0e731c2a1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 1132 additions and 601 deletions

View file

@ -1,108 +0,0 @@
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, Layer.succeed(A, A.of({ value: "a" })))
LayerNode.replace(notFoundOrDiskA, notFoundAImplementation)
LayerNode.replace(notFoundOrDiskA, diskAImplementation)
LayerNode.replaceWithNode(a, replacement)
// @ts-expect-error An override for A must still provide A
LayerNode.replaceWithNode(a, b)
// @ts-expect-error A replacement cannot introduce NetworkError
LayerNode.replace(notFoundOrDiskA, networkAImplementation)
// @ts-expect-error A replacement layer must not have unresolved dependencies
LayerNode.replace(b, bImplementation)
test("type exploration compiles", () => {})

View file

@ -1,204 +0,0 @@
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, replaceWithNode } = 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 = 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 = 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: [replaceWithNode(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 = 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: [replaceWithNode(value, replacement)] })).toThrow(
"Cycle detected in app graph: layer#1 -> layer#2 -> layer#1",
)
})
})

View file

@ -0,0 +1,19 @@
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, {}>()("test/TierA") {}
class B extends Context.Service<B, {}>()("test/TierB") {}
const tiers = LayerNode.tiers(["request", "global"])
const request = tiers.make("request")
const global = tiers.make("global")
const globalA = global({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
request({ service: B, layer: bLayer, deps: [globalA] })
// @ts-expect-error Global cannot depend on request
global({ service: B, layer: bLayer, deps: [request({ service: A, layer: Layer.succeed(A, A.of({})), deps: [] })] })
test("type exploration compiles", () => {})

View file

@ -0,0 +1,169 @@
import { expect, test } from "bun:test"
import { Context, Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
class Value extends Context.Service<Value, { readonly value: string }>()("test/TierValue") {}
class Result extends Context.Service<Result, { readonly value: string }>()("test/TierResult") {}
class Left extends Context.Service<Left, { readonly value: string }>()("test/TierLeft") {}
class Right extends Context.Service<Right, { readonly value: string }>()("test/TierRight") {}
class Last extends Context.Service<Last, { readonly value: string }>()("test/TierLast") {}
test("builds tiers with a custom builder", async () => {
let locationBuilds = 0
const tiers = LayerNode.tiers(["location", "global"])
const global = tiers.make("global")
const location = tiers.make("location")
const value = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "value" })), deps: [] })
const result = location({
service: Result,
layer: Layer.effect(
Result,
Effect.gen(function* () {
return Result.of({ value: (yield* Value).value })
}),
),
deps: [value],
})
const layer = LayerNode.buildLayer(LayerNode.group([result]), {
tiers,
buildTier: (tier, layers) => {
if (tier !== "location") return LayerNode.combine(layers)
locationBuilds++
return LayerNode.combine(layers).pipe(Layer.fresh)
},
})
const program = Effect.gen(function* () {
return (yield* Result).value
}).pipe(Effect.provide(layer))
expect(await Effect.runPromise(program)).toBe("value")
expect(locationBuilds).toBe(1)
})
test("rejects conflicting higher-tier service implementations", () => {
const tiers = LayerNode.tiers(["location", "global"])
const global = tiers.make("global")
const location = tiers.make("location")
const first = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "first" })), deps: [] })
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
const left = location({
service: Left,
layer: Layer.effect(Left, Effect.as(Value, Left.of({ value: "left" }))),
deps: [first],
})
const right = location({
service: Right,
layer: Layer.effect(Right, Effect.as(Value, Right.of({ value: "right" }))),
deps: [second],
})
expect(() => LayerNode.buildLayer(LayerNode.group([left, right]), { tiers })).toThrow(
"conflicting implementations for test/TierValue",
)
})
test("validates tier dependencies through groups", () => {
const tiers = LayerNode.tiers(["location", "global"])
const global = tiers.make("global")
const location = tiers.make("location")
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
const invalid = global({
service: Result,
layer: Layer.effect(
Result,
Effect.map(Value, (value) => Result.of({ value: value.value })),
),
deps: [LayerNode.group([local])],
})
expect(() => LayerNode.buildLayer(invalid, { tiers })).toThrow("Tier global cannot depend on lower tier location")
})
test("validates shared groups in each consumer tier", () => {
const tiers = LayerNode.tiers(["location", "global"])
const global = tiers.make("global")
const location = tiers.make("location")
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
const shared = LayerNode.group([local])
const valid = location({
service: Left,
layer: Layer.effect(
Left,
Effect.map(Value, (value) => Left.of({ value: value.value })),
),
deps: [shared],
})
const invalid = global({
service: Result,
layer: Layer.effect(
Result,
Effect.map(Value, (value) => Result.of({ value: value.value })),
),
deps: [shared],
})
expect(() => LayerNode.buildLayer(LayerNode.group([valid, invalid]), { tiers })).toThrow(
"Tier global cannot depend on lower tier location",
)
})
test("rejects a service assigned to multiple tiers", () => {
const tiers = LayerNode.tiers(["location", "global"])
const global = tiers.make("global")
const location = tiers.make("location")
const local = location({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "local" })), deps: [] })
const shared = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "global" })), deps: [] })
expect(() => LayerNode.buildLayer(LayerNode.group([local, shared]), { tiers })).toThrow(
"Service test/TierValue belongs to both tier location and tier global",
)
})
test("rebinds same-tier providers without reacquiring them", async () => {
let firstAcquisitions = 0
const tiers = LayerNode.tiers(["global"])
const global = tiers.make("global")
const first = global({
service: Value,
layer: Layer.effect(
Value,
Effect.sync(() => {
firstAcquisitions++
return Value.of({ value: "first" })
}),
),
deps: [],
})
const second = global({ service: Value, layer: Layer.succeed(Value, Value.of({ value: "second" })), deps: [] })
const left = global({
service: Left,
layer: Layer.effect(
Left,
Effect.map(Value, (value) => Left.of({ value: value.value })),
),
deps: [first],
})
const right = global({
service: Right,
layer: Layer.effect(
Right,
Effect.map(Value, (value) => Right.of({ value: value.value })),
),
deps: [second],
})
const last = global({
service: Last,
layer: Layer.effect(
Last,
Effect.map(Value, (value) => Last.of({ value: value.value })),
),
deps: [first],
})
const layer = LayerNode.buildLayer(LayerNode.group([left, right, last]), { tiers })
const values = Effect.gen(function* () {
return [(yield* Left).value, (yield* Right).value, (yield* Last).value]
}).pipe(Effect.provide(layer))
expect(await Effect.runPromise(values)).toEqual(["first", "second", "first"])
expect(firstAcquisitions).toBe(1)
})

View file

@ -0,0 +1,66 @@
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, {}>()("test/LayerNodeA") {}
class B extends Context.Service<B, {}>()("test/LayerNodeB") {}
class C extends Context.Service<C, {}>()("test/LayerNodeC") {}
class LayerError {
readonly _tag = "LayerError"
}
class OtherError {
readonly _tag = "OtherError"
}
const tiers = LayerNode.tiers(["app"])
const make = tiers.make("app")
const aLayer = Layer.succeed(A, A.of({}))
const bLayer = Layer.effect(B, Effect.as(A, B.of({})))
const cLayer = Layer.effect(
C,
Effect.gen(function* () {
yield* A
yield* B
return C.of({})
}),
)
const failingA = Layer.effect(A, Effect.fail(new LayerError()))
const a = make({ service: A, layer: aLayer, deps: [] })
const b = make({ service: B, layer: bLayer, deps: [a] })
const c = make({ service: C, layer: cLayer, deps: [a, b] })
const failing = make({ service: A, layer: failingA, deps: [] })
const dependent = make({ service: B, layer: bLayer, deps: [failing] })
make({ name: "manual-a", layer: aLayer, deps: [] })
// @ts-expect-error A node must have a service or name
make({ layer: aLayer, deps: [] })
// @ts-expect-error Service and name are mutually exclusive
make({ service: A, name: "a", layer: aLayer, deps: [] })
// @ts-expect-error B requires A
make({ service: B, layer: bLayer, deps: [] })
// @ts-expect-error C requires A and B
make({ service: C, layer: cLayer, deps: [a] })
const closed = LayerNode.buildLayer(c, { tiers })
const closedWithError = LayerNode.buildLayer(dependent, { tiers })
const checkClosed: Layer.Layer<C, never, never> = closed
const checkError: Layer.Layer<B, LayerError, never> = closedWithError
void checkClosed
void checkError
LayerNode.replace(aLayer, Layer.succeed(A, A.of({})))
// @ts-expect-error Replacement must provide A
LayerNode.replace(aLayer, Layer.succeed(B, B.of({})))
// @ts-expect-error Replacement cannot introduce a new error
LayerNode.replace(aLayer, Layer.effect(A, Effect.fail(new OtherError())))
// @ts-expect-error Replacement must be closed
LayerNode.replace(bLayer, bLayer)
test("type exploration compiles", () => {})

View file

@ -0,0 +1,82 @@
import { describe, expect, test } from "bun:test"
import { Context, Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
class Value extends Context.Service<Value, { readonly value: string }>()("test/LayerNodeValue") {}
class Greeting extends Context.Service<Greeting, { readonly value: string }>()("test/LayerNodeGreeting") {}
class Left extends Context.Service<Left, { readonly value: string }>()("test/LayerNodeLeft") {}
class Right extends Context.Service<Right, { readonly value: string }>()("test/LayerNodeRight") {}
const tiers = LayerNode.tiers(["app"])
const make = tiers.make("app")
const valueLayer = Layer.succeed(Value, Value.of({ value: "production" }))
const greetingLayer = Layer.effect(
Greeting,
Effect.map(Value, (value) => Greeting.of({ value: `hello ${value.value}` })),
)
const value = make({ service: Value, layer: valueLayer, deps: [] })
const greeting = make({ service: Greeting, layer: greetingLayer, deps: [value] })
describe("layer node", () => {
test("builds an untiered graph", async () => {
const value = LayerNode.make({ service: Value, layer: valueLayer, deps: [] })
const greeting = LayerNode.make({ service: Greeting, layer: greetingLayer, deps: [value] })
const program = Effect.map(Greeting, (item) => item.value).pipe(Effect.provide(LayerNode.buildLayer(greeting)))
expect(await Effect.runPromise(program)).toBe("hello production")
})
test("builds a dependency graph", async () => {
const program = Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(LayerNode.buildLayer(greeting, { tiers })),
)
expect(await Effect.runPromise(program)).toBe("hello production")
})
test("replaces a layer by identity", async () => {
const replacement = Layer.succeed(Value, Value.of({ value: "simulation" }))
const program = Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(valueLayer, replacement)] })),
)
expect(await Effect.runPromise(program)).toBe("hello simulation")
})
test("replaces every use of the same layer", async () => {
const leftLayer = Layer.effect(
Left,
Effect.map(Value, (item) => Left.of({ value: item.value })),
)
const rightLayer = Layer.effect(
Right,
Effect.map(Value, (item) => Right.of({ value: item.value })),
)
const left = make({ service: Left, layer: leftLayer, deps: [value] })
const right = make({ service: Right, layer: rightLayer, deps: [value] })
const replacement = Layer.succeed(Value, Value.of({ value: "replaced" }))
const layer = LayerNode.buildLayer(LayerNode.group([left, right]), {
tiers,
replacements: [LayerNode.replace(valueLayer, replacement)],
})
const program = Effect.gen(function* () {
return [(yield* Left).value, (yield* Right).value]
}).pipe(Effect.provide(layer))
expect(await Effect.runPromise(program)).toEqual(["replaced", "replaced"])
})
test("does not acquire an unused replacement", async () => {
let acquisitions = 0
const other = Layer.succeed(Value, Value.of({ value: "other" }))
const replacement = Layer.effect(
Value,
Effect.sync(() => {
acquisitions++
return Value.of({ value: "replacement" })
}),
)
await Effect.runPromise(
Effect.map(Greeting, (item) => item.value).pipe(
Effect.provide(LayerNode.buildLayer(greeting, { tiers, replacements: [LayerNode.replace(other, replacement)] })),
),
)
expect(acquisitions).toBe(0)
})
})

View file

@ -0,0 +1,23 @@
import { test } from "bun:test"
import { Context, Effect, Layer } from "effect"
import { makeGlobalNode, makeLocationNode } from "@opencode-ai/core/effect/scoped-node"
class A extends Context.Service<A, {}>()("test/ScopedA") {}
class B extends Context.Service<B, {}>()("test/ScopedB") {}
const a = Layer.succeed(A, A.of({}))
const b = Layer.effect(B, Effect.as(A, B.of({})))
const globalA = makeGlobalNode({ service: A, layer: a, deps: [] })
const locationA = makeLocationNode({ service: A, layer: a, deps: [] })
makeGlobalNode({ service: B, layer: b, deps: [globalA] })
makeLocationNode({ service: B, layer: b, deps: [globalA] })
makeLocationNode({ service: B, layer: b, deps: [locationA] })
// @ts-expect-error Global nodes cannot depend on location nodes
makeGlobalNode({ service: B, layer: b, deps: [locationA] })
// @ts-expect-error B requires A
makeLocationNode({ service: B, layer: b, deps: [] })
test("type exploration compiles", () => {})

View file

@ -177,10 +177,13 @@ const root = LayerNode.group([
CrossSpawnSpawner.node,
])
const replacements = [
LayerNode.replace(SessionSummary.node, summary),
LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })),
LayerNode.replace(SessionSummary.layer, summary),
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
]
const env = LayerNode.buildLayer(LayerNode.group([root, LayerNode.make(TestLLMServer.layer, [])]), { replacements })
const env = LayerNode.buildLayer(
LayerNode.group([root, LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })]),
{ replacements },
)
const it = testEffect(env)
@ -205,7 +208,7 @@ const providerErrorLLM = Layer.succeed(
}),
)
const providerErrorEnv = LayerNode.buildLayer(root, {
replacements: [...replacements, LayerNode.replace(LLM.node, providerErrorLLM)],
replacements: [...replacements, LayerNode.replace(LLM.layer, providerErrorLLM)],
})
const itProviderError = testEffect(providerErrorEnv)
@ -224,7 +227,7 @@ const fragmentFailureLLM = Layer.succeed(
}),
)
const fragmentFailureEnv = LayerNode.buildLayer(root, {
replacements: [...replacements, LayerNode.replace(LLM.node, fragmentFailureLLM)],
replacements: [...replacements, LayerNode.replace(LLM.layer, fragmentFailureLLM)],
})
const itFragmentFailure = testEffect(fragmentFailureEnv)

View file

@ -84,14 +84,14 @@ const root = LayerNode.group([
SessionSummary.node,
Database.node,
CrossSpawnSpawner.node,
LayerNode.make(TestLLMServer.layer, []),
LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] }),
])
const it = testEffect(
LayerNode.buildLayer(root, {
replacements: [
LayerNode.replace(MCP.node, mcp),
LayerNode.replace(LSP.node, lsp),
LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })),
LayerNode.replace(MCP.layer, mcp),
LayerNode.replace(LSP.layer, lsp),
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer({ experimentalEventSystem: true })),
],
}),
)

View file

@ -1,6 +1,6 @@
import { beforeEach, describe, expect } from "bun:test"
import { Effect, Exit, Layer, Option } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/layer-node-platform"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@ -35,7 +35,7 @@ const none = HttpClient.make(() => Effect.die("unexpected http call"))
function requestLayer(client: HttpClient.HttpClient) {
return LayerNode.buildLayer(LayerNode.group([ShareNext.node, AccountRepo.node]), {
replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))],
replacements: [LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))],
})
}
@ -49,9 +49,7 @@ function integrationLayer(client: HttpClient.HttpClient) {
AccountRepo.node,
Database.node,
]),
{
replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))],
},
{ replacements: [LayerNode.replace(FetchHttpClient.layer, Layer.succeed(HttpClient.HttpClient, client))] },
)
}

View file

@ -50,14 +50,14 @@ const brokenPluginLayer = Layer.succeed(
const root = LayerNode.group([ToolRegistry.node, Agent.node])
const replacements = [
LayerNode.replace(Config.node, configLayer),
LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer()),
LayerNode.replace(Config.layer, configLayer),
LayerNode.replace(RuntimeFlags.defaultLayer, RuntimeFlags.layer()),
]
const it = testEffect(LayerNode.buildLayer(root, { replacements }))
const withBrokenPlugin = testEffect(
LayerNode.buildLayer(root, {
replacements: [...replacements, LayerNode.replace(Plugin.node, brokenPluginLayer)],
replacements: [...replacements, LayerNode.replace(Plugin.layer, brokenPluginLayer)],
}),
)