skill: use Effect.cached for load deduplication (#19165)
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +111,7 @@ 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 => {
|
|
||||||
const state: State = {
|
|
||||||
skills: {},
|
|
||||||
dirs: new Set<string>(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
|
if (!Flag.OPENCODE_DISABLE_EXTERNAL_SKILLS) {
|
||||||
for (const dir of EXTERNAL_DIRS) {
|
for (const dir of EXTERNAL_DIRS) {
|
||||||
const root = path.join(Global.Path.home, dir)
|
const root = path.join(Global.Path.home, dir)
|
||||||
@@ -166,18 +154,6 @@ export namespace Skill {
|
|||||||
log.info("init", { count: Object.keys(state.skills).length })
|
log.info("init", { count: Object.keys(state.skills).length })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensure = () => {
|
|
||||||
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") {}
|
||||||
|
|
||||||
export const layer: Layer.Layer<Service, never, Discovery.Service> = Layer.effect(
|
export const layer: Layer.Layer<Service, never, Discovery.Service> = Layer.effect(
|
||||||
@@ -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")
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user