skill: use Effect.cached for load deduplication (#19165)

This commit is contained in:
Kit Langton
2026-03-26 00:55:43 +00:00
committed by GitHub
parent 05c3cfb2aa
commit ea04b23745
3 changed files with 50 additions and 74 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ import z from "zod"
import { Global } from "../global" import { Global } from "../global"
import { Effect, Layer, ServiceMap } from "effect" import { Effect, Layer, ServiceMap } from "effect"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { makeRunPromise } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
export namespace McpAuth { export namespace McpAuth {
export const Tokens = z.object({ export const Tokens = z.object({
@@ -143,7 +143,7 @@ export namespace McpAuth {
const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
const runPromise = makeRunPromise(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
// Async facades for backward compat (used by McpOAuthProvider, CLI) // Async facades for backward compat (used by McpOAuthProvider, CLI)
+2 -2
View File
@@ -26,7 +26,7 @@ import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open" import open from "open"
import { Effect, Layer, Option, ServiceMap, Stream } from "effect" import { Effect, Layer, Option, ServiceMap, Stream } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRunPromise } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
@@ -893,7 +893,7 @@ export namespace MCP {
Layer.provide(NodePath.layer), Layer.provide(NodePath.layer),
) )
const runPromise = makeRunPromise(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
// --- Async facade functions --- // --- Async facade functions ---
+46 -70
View File
@@ -54,11 +54,6 @@ export namespace Skill {
type State = { type State = {
skills: Record<string, Info> skills: Record<string, Info>
dirs: Set<string> dirs: Set<string>
task?: Promise<void>
}
type Cache = State & {
ensure: () => Promise<void>
} }
export interface Interface { export interface Interface {
@@ -116,66 +111,47 @@ export namespace Skill {
}) })
} }
// TODO: Migrate to Effect async function loadSkills(state: State, discovery: Discovery.Interface, directory: string, worktree: string) {
const create = (discovery: Discovery.Interface, directory: string, worktree: string): Cache => { if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
const state: State = { for (const dir of EXTERNAL_DIRS) {
skills: {}, const root = path.join(Global.Path.home, dir)
dirs: new Set<string>(), if (!(await Filesystem.isDir(root))) continue
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
}
for await (const root of Filesystem.up({
targets: EXTERNAL_DIRS,
start: directory,
stop: worktree,
})) {
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" })
}
} }
const load = async () => { for (const dir of await Config.directories()) {
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) { await scan(state, dir, OPENCODE_SKILL_PATTERN)
for (const dir of EXTERNAL_DIRS) { }
const root = path.join(Global.Path.home, dir)
if (!(await Filesystem.isDir(root))) continue
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "global" })
}
for await (const root of Filesystem.up({ const cfg = await Config.get()
targets: EXTERNAL_DIRS, for (const item of cfg.skills?.paths ?? []) {
start: directory, const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
stop: worktree, const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
})) { if (!(await Filesystem.isDir(dir))) {
await scan(state, root, EXTERNAL_SKILL_PATTERN, { dot: true, scope: "project" }) log.warn("skill path not found", { path: dir })
} continue
} }
for (const dir of await Config.directories()) { await scan(state, dir, SKILL_PATTERN)
await scan(state, dir, OPENCODE_SKILL_PATTERN) }
}
const cfg = await Config.get()
for (const item of cfg.skills?.paths ?? []) {
const expanded = item.startsWith("~/") ? path.join(os.homedir(), item.slice(2)) : item
const dir = path.isAbsolute(expanded) ? expanded : path.join(directory, expanded)
if (!(await Filesystem.isDir(dir))) {
log.warn("skill path not found", { path: dir })
continue
}
for (const url of cfg.skills?.urls ?? []) {
for (const dir of await Effect.runPromise(discovery.pull(url))) {
state.dirs.add(dir)
await scan(state, dir, SKILL_PATTERN) await scan(state, dir, SKILL_PATTERN)
} }
for (const url of cfg.skills?.urls ?? []) {
for (const dir of await Effect.runPromise(discovery.pull(url))) {
state.dirs.add(dir)
await scan(state, dir, SKILL_PATTERN)
}
}
log.info("init", { count: Object.keys(state.skills).length })
} }
const ensure = () => { log.info("init", { count: Object.keys(state.skills).length })
if (state.task) return state.task
state.task = load().catch((err) => {
state.task = undefined
throw err
})
return state.task
}
return { ...state, ensure }
} }
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Skill") {} export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Skill") {}
@@ -185,33 +161,33 @@ export namespace Skill {
Effect.gen(function* () { Effect.gen(function* () {
const discovery = yield* Discovery.Service const discovery = yield* Discovery.Service
const state = yield* InstanceState.make( const state = yield* InstanceState.make(
Effect.fn("Skill.state")((ctx) => Effect.sync(() => create(discovery, ctx.directory, ctx.worktree))), Effect.fn("Skill.state")((ctx) =>
Effect.gen(function* () {
const s: State = { skills: {}, dirs: new Set() }
yield* Effect.promise(() => loadSkills(s, discovery, ctx.directory, ctx.worktree))
return s
}),
),
) )
const ensure = Effect.fn("Skill.ensure")(function* () {
const cache = yield* InstanceState.get(state)
yield* Effect.promise(() => cache.ensure())
return cache
})
const get = Effect.fn("Skill.get")(function* (name: string) { const get = Effect.fn("Skill.get")(function* (name: string) {
const cache = yield* ensure() const s = yield* InstanceState.get(state)
return cache.skills[name] return s.skills[name]
}) })
const all = Effect.fn("Skill.all")(function* () { const all = Effect.fn("Skill.all")(function* () {
const cache = yield* ensure() const s = yield* InstanceState.get(state)
return Object.values(cache.skills) return Object.values(s.skills)
}) })
const dirs = Effect.fn("Skill.dirs")(function* () { const dirs = Effect.fn("Skill.dirs")(function* () {
const cache = yield* ensure() const s = yield* InstanceState.get(state)
return Array.from(cache.dirs) return Array.from(s.dirs)
}) })
const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) { const available = Effect.fn("Skill.available")(function* (agent?: Agent.Info) {
const cache = yield* ensure() const s = yield* InstanceState.get(state)
const list = Object.values(cache.skills).toSorted((a, b) => a.name.localeCompare(b.name)) const list = Object.values(s.skills).toSorted((a, b) => a.name.localeCompare(b.name))
if (!agent) return list if (!agent) return list
return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny") return list.filter((skill) => Permission.evaluate("skill", skill.name, agent.permission).action !== "deny")
}) })