refactor(core): finish test layer node conversion (#34385)

This commit is contained in:
James Long 2026-06-29 11:35:17 -04:00 committed by GitHub
commit a3776429aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 728 additions and 602 deletions

View file

@ -1,6 +1,7 @@
export * as MoveSession from "./move-session"
import { Context, DateTime, Effect, Layer, Schema } from "effect"
import { makeGlobalNode } from "../effect/app-node"
import { EventV2 } from "../event"
import { Git } from "../git"
import { Location } from "../location"
@ -146,3 +147,9 @@ export const defaultLayer = layer.pipe(
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(SessionStore.defaultLayer),
)
export const node = makeGlobalNode({
service: Service,
layer,
deps: [Git.node, EventV2.node, ProjectV2.node, SessionStore.node],
})

View file

@ -272,7 +272,48 @@ export function compile<A, E, const Items extends Replacements = readonly []>(
}
function replacementMapFrom(replacements?: Replacements) {
return new Map(replacements?.map(([source, replacement]) => [source.name, replacementNode(source, replacement)]))
return replacements?.reduce((map, [source, replacement]) => {
const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map)
const current = new Map([[source.name, normalized]])
for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current))
map.set(source.name, normalized)
return map
}, new Map<string, AnyNode>()) ?? new Map<string, AnyNode>()
}
function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap<string, AnyNode>) {
if (replacements.size === 0) return root
const cache = new Map<AnyNode, AnyNode>()
const visiting = new Set<AnyNode>()
const stack: AnyNode[] = []
const recur = (node: AnyNode, isRoot = false): AnyNode => {
const target = isRoot ? node : (replacements.get(node.name) ?? node)
const cached = cache.get(target)
if (cached !== undefined || cache.has(target)) return cached!
if (visiting.has(target)) {
const start = stack.indexOf(target)
throw new Error(
`Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`,
)
}
visiting.add(target)
stack.push(target)
try {
const dependencies = target.dependencies.map((dependency) => recur(dependency))
const result = dependencies.every((dependency, index) => dependency === target.dependencies[index])
? target
: { ...target, dependencies }
cache.set(target, result)
return result
} finally {
stack.pop()
visiting.delete(target)
}
}
return recur(root, true)
}
export function hasUnbound(root: Node<unknown, unknown, any>, source: AnyNode): boolean {

View file

@ -4,11 +4,13 @@ import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { Patch } from "../patch"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -194,6 +196,12 @@ export const layer = Layer.effectDiscard(
}),
)
export const node = makeLocationNode({
name: "tool/apply-patch",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})
function patchFile(change: Prepared): typeof FileDiff.Info.Type {
const counts = diffLines(change.before, change.after).reduce(
(result, item) => ({

View file

@ -5,11 +5,13 @@ import { ToolFailure } from "@opencode-ai/llm"
import { Duration, Effect, Layer, Schema } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { Config } from "../config"
import { makeLocationNode } from "../effect/app-node"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { AppProcess } from "../process"
import { PermissionV2 } from "../permission"
import { PositiveInt } from "../schema"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -193,3 +195,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/bash",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FSUtil.node, AppProcess.node, Config.node, PermissionV2.node],
})

View file

@ -15,20 +15,6 @@ import { TodoWriteTool } from "./todowrite"
import { WebFetchTool } from "./webfetch"
import { WebSearchTool } from "./websearch"
import { WriteTool } from "./write"
import { FSUtil } from "../fs-util"
import { AppProcess } from "../process"
import { Config } from "../config"
import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { FileMutation } from "../file-mutation"
import { PermissionV2 } from "../permission"
import { Ripgrep } from "../ripgrep"
import { Image } from "../image"
import { QuestionV2 } from "../question"
import { SkillV2 } from "../skill"
import { SessionTodo } from "../session/todo"
import { ToolRegistry } from "./registry"
import { httpClient } from "../effect/app-node-platform"
/**
* Composes only the shipped Location-scoped built-in tool transforms.
@ -60,22 +46,19 @@ export const locationLayer = Layer.mergeAll(
export const node = makeLocationNode({
name: "built-in-tools",
layer: locationLayer,
layer: Layer.empty,
deps: [
ToolRegistry.toolsNode,
FSUtil.node,
AppProcess.node,
Config.node,
Location.node,
LocationMutation.node,
FileMutation.node,
PermissionV2.node,
Ripgrep.node,
Image.node,
QuestionV2.node,
SkillV2.node,
SessionTodo.node,
ReadToolFileSystem.node,
httpClient,
ApplyPatchTool.node,
BashTool.node,
EditTool.node,
GlobTool.node,
GrepTool.node,
QuestionTool.node,
ReadTool.node,
SkillTool.node,
TodoWriteTool.node,
WebFetchTool.node,
WebSearchTool.node,
WriteTool.node,
],
})

View file

@ -10,10 +10,12 @@ import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -213,3 +215,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/edit",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})

View file

@ -3,11 +3,13 @@ export * as GlobTool from "./glob"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import path from "path"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { Location } from "../location"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -95,3 +97,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/glob",
layer,
deps: [ToolRegistry.node, Ripgrep.node, Location.node, PermissionV2.node],
})

View file

@ -3,12 +3,14 @@ export * as GrepTool from "./grep"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import path from "path"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -127,3 +129,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/grep",
layer,
deps: [ToolRegistry.node, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
})

View file

@ -2,8 +2,10 @@ export * as QuestionTool from "./question"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { PermissionV2 } from "../permission"
import { QuestionV2 } from "../question"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -84,3 +86,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/question",
layer,
deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node],
})

View file

@ -2,12 +2,14 @@ export * as ReadTool from "./read"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { Image } from "../image"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { AbsolutePath } from "../schema"
import { ReadToolFileSystem } from "./read-filesystem"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -107,3 +109,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/read",
layer,
deps: [ToolRegistry.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, PermissionV2.node],
})

View file

@ -3,9 +3,11 @@ export * as SkillTool from "./skill"
import path from "path"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FSUtil } from "../fs-util"
import { SkillV2 } from "../skill"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -99,3 +101,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/skill",
layer,
deps: [ToolRegistry.node, FSUtil.node, SkillV2.node, PermissionV2.node],
})

View file

@ -2,8 +2,10 @@ export * as TodoWriteTool from "./todowrite"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { PermissionV2 } from "../permission"
import { SessionTodo } from "../session/todo"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -52,3 +54,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/todowrite",
layer,
deps: [ToolRegistry.node, PermissionV2.node, SessionTodo.node],
})

View file

@ -5,8 +5,11 @@ import { Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Parser } from "htmlparser2"
import TurndownService from "turndown"
import { makeLocationNode } from "../effect/app-node"
import { LayerNodePlatform } from "../effect/app-node-platform"
import { PermissionV2 } from "../permission"
import { collectBoundedResponseBody } from "./http-body"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -177,6 +180,12 @@ export const layer = Layer.effectDiscard(
}),
)
export const node = makeLocationNode({
name: "tool/webfetch",
layer,
deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient],
})
export function extractTextFromHTML(html: string) {
let text = ""
let skipDepth = 0

View file

@ -3,6 +3,8 @@ export * as WebSearchTool from "./websearch"
import { ToolFailure } from "@opencode-ai/llm"
import { Context, Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { makeLocationNode } from "../effect/app-node"
import { LayerNodePlatform } from "../effect/app-node-platform"
import { truthy } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { PositiveInt } from "../schema"
@ -11,6 +13,7 @@ import { Tool } from "./tool"
import { Tools } from "./tools"
import { collectBoundedResponseBody } from "./http-body"
import { checksum } from "../util/encode"
import { ToolRegistry } from "./registry"
export const name = "websearch"
export const NO_RESULTS = "No search results found. Please try a different query."
@ -80,6 +83,8 @@ export const defaultConfigLayer = Layer.sync(ConfigService, () =>
}),
)
export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] })
export function selectProvider(
sessionID: string,
flags: Pick<Config, "enableExa" | "enableParallel"> = { enableExa: false, enableParallel: false },
@ -247,3 +252,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/websearch",
layer,
deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, configNode],
})

View file

@ -8,9 +8,11 @@ export * as WriteTool from "./write"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -91,3 +93,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/write",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, PermissionV2.node],
})