Skip to content
Open
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
11 changes: 6 additions & 5 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,13 @@ Effect.gen(function* () {
Effect.provide(Config.layer),
Effect.provide(Updater.layer),
Effect.provide(
LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]), [
[
Global.node,
Global.layerWith(process.env.OPENCODE_CONFIG_DIR ? { config: process.env.OPENCODE_CONFIG_DIR } : {}),
LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]), {
replacements: [
Global.node.replace(
Global.layerWith(process.env.OPENCODE_CONFIG_DIR ? { config: process.env.OPENCODE_CONFIG_DIR } : {}),
),
],
]),
}),
),
Effect.provide(
Observability.layer({
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/server-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ export const run = Effect.fnUntraced(function* (options: Options) {
return yield* processEffect(options).pipe(
Effect.provide(Updater.layer),
Effect.provide(
LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]), [
[
Global.node,
Global.layerWith(process.env.OPENCODE_CONFIG_DIR ? { config: process.env.OPENCODE_CONFIG_DIR } : {}),
LayerNode.compile(LayerNode.group([Global.node, AppProcess.node]), {
replacements: [
Global.node.replace(
Global.layerWith(process.env.OPENCODE_CONFIG_DIR ? { config: process.env.OPENCODE_CONFIG_DIR } : {}),
),
],
]),
}),
),
Effect.provide(NodeServices.layer),
)
Expand Down
17 changes: 4 additions & 13 deletions packages/core/src/effect/app-node-builder.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import { buildLocationServiceMap } from "../location-services.js"
import { LocationServiceMap } from "../location-service-map.js"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"

export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
// Only build the location service map if it's actually needed
if (!LayerNode.hasUnbound(root, LocationServiceMap.node) || hasReplacement(replacements, LocationServiceMap.node))
return LayerNode.compile(root, replacements)

const locationMap = buildLocationServiceMap(replacements)
const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] })
return LayerNode.compile(root, replacements.concat([[LocationServiceMap.node, locationMapNode]]))
}

function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node<unknown, unknown, any>) {
return replacements.some(([source]) => source.name === node.name)
export function build<A, E>(root: LayerNode.Graph<A, E>, replacements: LayerNode.Replacements = []) {
return LayerNode.compile(root, {
replacements: [LocationServiceMap.node.replace(buildLocationServiceMap(replacements)), ...replacements],
})
}

export * as AppNodeBuilder from "./app-node-builder.js"
25 changes: 9 additions & 16 deletions packages/core/src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ const nodes = [
Vcs.node,
// Start repository watches only after boot-critical filesystem and Git work.
LocationWatcher.node,
] as const satisfies readonly Node.LocationNode<unknown, unknown>[]
] as const satisfies readonly Node.LocationGraph<never, unknown>[]

export const graph = LayerNode.group<typeof nodes>(nodes)
export const graph = LayerNode.group(nodes)

export type Services = LayerNode.Output<typeof graph>
export type Error = LayerNode.Error<typeof graph>
Expand Down Expand Up @@ -139,36 +139,29 @@ export interface Options {
// source still honors explicit plugin operations from wellknown and
// host-injected config.
const vanillaReplacements: LayerNode.Replacements = [
[Config.node, Config.configured({ project: false, global: false })],
[InstructionDiscovery.node, InstructionDiscovery.configured({ project: false, global: false })],
Config.node.replace(Config.configured({ project: false, global: false })),
InstructionDiscovery.node.replace(InstructionDiscovery.configured({ project: false, global: false })),
]

// One instance is one compiled, fresh copy of the graph standing on a directory.
export function layer(ref: Location.Ref, options: Options = {}) {
const startedAt = performance.now()
// Ordered: vanilla defaults, then caller replacements (which win over the
// defaults), then bound pairs (which win over everything).
const allReplacements: LayerNode.Replacements = [
// defaults), then instance bindings (which win over everything).
const replacements: LayerNode.Replacements = [
...(options.discovery === false ? vanillaReplacements : []),
...(options.replacements ?? []),
[Location.node, Location.boundNode(ref, { discovery: options.discovery })],
[InstancePlugins.node, InstancePlugins.bound(options.plugins ?? [])],
Location.node.replace(Location.boundNode(ref, { discovery: options.discovery })),
InstancePlugins.node.replace(InstancePlugins.bound(options.plugins ?? [])),
]
// Apply replacements during hoist, not afterward: replacements can
// introduce new tagged dependencies (Location.boundNode depends on
// Project), and the hoist walk is the only pass that can still slice
// those back out.
const location = LayerNode.hoist(graph, Node.tags.values.global, allReplacements)

return LayerNode.compile(location.node).pipe(
Layer.fresh,
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global }).pipe(
Layer.tap(() =>
Effect.logInfo("location services booted", {
directory: ref.directory,
workspaceID: ref.workspaceID,
durationMs: Math.round(performance.now() - startedAt),
}),
),
Layer.provide(LayerNode.compile(location.hoisted)),
)
}
12 changes: 9 additions & 3 deletions packages/core/src/persistent-pty/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export interface Interface {

export class Service extends Context.Service<Service, Interface>()("@opencode/PersistentPty") {}

export const configured = (options: Options = {}) =>
const makeLayer = (options: Options = {}) =>
Layer.effect(
Service,
Effect.gen(function* () {
Expand Down Expand Up @@ -361,8 +361,14 @@ export const configured = (options: Options = {}) =>
}),
)

export const layer = configured()
export const node = makeGlobalNode({ service: Service, layer, deps: [Bus.node, Global.node] })
export const layer = makeLayer()
export const configured = (options?: Options) =>
makeGlobalNode({
service: Service,
layer: options === undefined ? layer : makeLayer(options),
deps: [Bus.node, Global.node],
})
export const node = configured()

const request = (daemon: DaemonTransport, value: object, start = false) =>
daemon.request(value, start).pipe(Effect.mapError(unavailable))
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const globalLayer = Layer.succeed(Global.Service, Global.Service.of(global))

const it = testEffect(
AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [
[Global.node, globalLayer],
[Location.node, locationLayer],
Global.node.replace(globalLayer),
Location.node.replace(locationLayer),
]) as unknown as Layer.Layer<unknown, never>,
)

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/bus-session-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { testEffect } from "./lib/effect"

const it = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]), [
[Bus.node, Bus.configured({ persist: true })],
Bus.node.replace(Bus.configured({ persist: true })),
]),
)
const a = Location.Ref.make({ directory: AbsolutePath.make("/a") })
Expand Down
20 changes: 10 additions & 10 deletions packages/core/test/bus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ const tail = (bus: Bus.Interface, input: { aggregateID: string; after?: number }

const it = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, Location.node]), [
[Location.node, locationLayer],
[Bus.node, Bus.configured({ persist: true })],
Location.node.replace(locationLayer),
Bus.node.replace(Bus.configured({ persist: true })),
]),
)
const itWithoutLocation = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]), [[Bus.node, Bus.configured({ persist: true })]]),
AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]), [
Bus.node.replace(Bus.configured({ persist: true })),
]),
)
const itWithoutPersistence = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node])))

Expand Down Expand Up @@ -631,16 +633,15 @@ describe("Bus", () => {
const continueRead = yield* Deferred.make<void>()
let pause = true
const eventLayer = AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]), [
[
Bus.node,
Bus.node.replace(
Bus.configured({
persist: true,
beforeAggregateRead: () =>
pause
? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead)))
: Effect.void,
}),
],
),
])

yield* Effect.gen(function* () {
Expand Down Expand Up @@ -1318,7 +1319,7 @@ describe("Bus", () => {
it.effect("log replays across configured read pages", () =>
Effect.gen(function* () {
const eventLayer = AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]), [
[Bus.node, Bus.configured({ persist: true, logReadPageSize: 2 })],
Bus.node.replace(Bus.configured({ persist: true, logReadPageSize: 2 })),
])

yield* Effect.gen(function* () {
Expand Down Expand Up @@ -1351,8 +1352,7 @@ describe("Bus", () => {
const releaseRead = yield* Deferred.make<void>()
const firstRead = yield* Ref.make(true)
const eventLayer = AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]), [
[
Bus.node,
Bus.node.replace(
Bus.configured({
persist: true,
beforeAggregateRead: () =>
Expand All @@ -1363,7 +1363,7 @@ describe("Bus", () => {
}),
),
}),
],
),
])

yield* Effect.gen(function* () {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/test/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const locationLayer = Layer.succeed(
)
const catalogLayer = AppNodeBuilder.build(
LayerNode.group([Catalog.node, Bus.node, Credential.node, Integration.node]),
[[Location.node, locationLayer]],
[Location.node.replace(locationLayer)],
)
const it = testEffect(catalogLayer)

Expand All @@ -48,7 +48,7 @@ describe("Catalog", () => {
it.effect("derives availability from active credentials without changing provider state", () => {
const integrationID = Integration.ID.make("test")
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node]), [[Location.node, locationLayer]]),
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node]), [Location.node.replace(locationLayer)]),
)

return Effect.gen(function* () {
Expand Down Expand Up @@ -78,7 +78,7 @@ describe("Catalog", () => {
const providerID = Provider.ID.make("remote")
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [
[Location.node, locationLayer],
Location.node.replace(locationLayer),
]),
)

Expand Down Expand Up @@ -108,7 +108,7 @@ describe("Catalog", () => {
const providerID = Provider.ID.make("remote")
const localCatalogLayer = Layer.fresh(
AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [
[Location.node, locationLayer],
Location.node.replace(locationLayer),
]),
)

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/codemode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("CodeMode", () => {
Effect.scoped,
Effect.provide(
AppNodeBuilder.build(Tool.node, [
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
]),
),
),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/codemode/instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("CodeModeInstructions", () => {
execute: () => Effect.succeed({ output: "zeta" }),
}
const layer = AppNodeBuilder.build(Tool.node, [
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
Location.node.replace(Location.boundNode({ directory: AbsolutePath.make("/project") })),
])

return Effect.gen(function* () {
Expand Down
21 changes: 10 additions & 11 deletions packages/core/test/config/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([Command.node, Bus.node, FSUtil.node, AppProcess.node, Location.node, ShellSelect.node]),
[
[Mcp.node, emptyMcpLayer],
[Config.node, emptyConfigLayer],
[Location.node, testLocationLayer],
[ShellSelect.node, shellLayer],
Mcp.node.replace(emptyMcpLayer),
Config.node.replace(emptyConfigLayer),
Location.node.replace(testLocationLayer),
ShellSelect.node.replace(shellLayer),
],
),
)
Expand Down Expand Up @@ -340,17 +340,16 @@ describeNative("ConfigCommandPlugin native watcher", () => {
ShellSelect.node,
]),
[
[
Location.node,
Location.node.replace(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(path.join(tmp, "project")) })),
),
],
[Global.node, Global.layerWith({ config: global, home: path.join(global, "home") })],
[ShellSelect.node, shellLayer],
[Credential.node, emptyCredentialNode],
[WellKnown.node, emptyWellknownNode],
),
Global.node.replace(Global.layerWith({ config: global, home: path.join(global, "home") })),
ShellSelect.node.replace(shellLayer),
Credential.node.replace(emptyCredentialNode),
WellKnown.node.replace(emptyWellknownNode),
],
),
),
Expand Down
7 changes: 3 additions & 4 deletions packages/core/test/config/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ const it = testEffect(
Layer.merge(
config,
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, SessionModelRequest.node, Config.node, Bus.node]), [
[
llmClient,
llmClient.replace(
Layer.mock(LLMClient.Service)({
stream: () => Stream.make(LLMEvent.textDelta({ id: "summary", text: "summary" })),
}),
],
[Config.node, config],
),
Config.node.replace(config),
]),
),
)
Expand Down
23 changes: 11 additions & 12 deletions packages/core/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ function testLayer(
),
)
const built = AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
[Config.node, Config.configured(options)],
[Location.node, locationLayer],
[Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })],
[Credential.node, credentialNode],
[WellKnown.node, wellknownNode],
[Watcher.node, watcher],
Config.node.replace(Config.configured(options)),
Location.node.replace(locationLayer),
Global.node.replace(Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })),
Credential.node.replace(credentialNode),
WellKnown.node.replace(wellknownNode),
Watcher.node.replace(watcher),
])
// Merge the watcher layer by reference so Watcher.Test resolves to the same
// memoized instance the built graph uses.
Expand Down Expand Up @@ -311,16 +311,15 @@ describe("Config", () => {
}).pipe(
Effect.provide(
AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [
[
Location.node,
Location.node.replace(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(project) })),
),
],
[Global.node, Global.layerWith({ config: global, home: path.join(global, "home") })],
[Credential.node, emptyCredentialNode],
[WellKnown.node, emptyWellknownNode],
),
Global.node.replace(Global.layerWith({ config: global, home: path.join(global, "home") })),
Credential.node.replace(emptyCredentialNode),
WellKnown.node.replace(emptyWellknownNode),
]),
),
)
Expand Down
Loading
Loading