fix(core): reload all config plugins
This commit is contained in:
@@ -2,7 +2,7 @@ export * as ConfigAgentPlugin from "./agent"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Option, Schema } from "effect"
|
import { Effect, Option, Schema, Stream } from "effect"
|
||||||
import { AgentV2 } from "../../agent"
|
import { AgentV2 } from "../../agent"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ConfigAgent } from "../agent"
|
import { ConfigAgent } from "../agent"
|
||||||
@@ -38,7 +38,8 @@ export const Plugin = define({
|
|||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
|
const load = Effect.fn("ConfigAgentPlugin.load")(function* () {
|
||||||
|
return yield* Effect.forEach(yield* config.entries(), (entry) => {
|
||||||
if (entry.type === "document") return Effect.succeed([entry])
|
if (entry.type === "document") return Effect.succeed([entry])
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const files = yield* discover(fs, entry.path)
|
const files = yield* discover(fs, entry.path)
|
||||||
@@ -54,15 +55,17 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}).pipe(Effect.map((documents) => documents.flat()))
|
}).pipe(Effect.map((documents) => documents.flat()))
|
||||||
const global = documents.flatMap((document) => document.info.permissions ?? [])
|
})
|
||||||
const configuredDefault = Config.latest(documents, "default_agent")
|
const loaded = { documents: yield* load() }
|
||||||
yield* ctx.agent.transform((draft) => {
|
yield* ctx.agent.transform((draft) => {
|
||||||
|
const global = loaded.documents.flatMap((document) => document.info.permissions ?? [])
|
||||||
|
const configuredDefault = Config.latest(loaded.documents, "default_agent")
|
||||||
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
||||||
for (const current of draft.list()) {
|
for (const current of draft.list()) {
|
||||||
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
draft.update(current.id, (agent) => agent.permissions.push(...global))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const document of documents) {
|
for (const document of loaded.documents) {
|
||||||
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
for (const [id, item] of Object.entries(document.info.agents ?? {})) {
|
||||||
const agentID = AgentV2.ID.make(id)
|
const agentID = AgentV2.ID.make(id)
|
||||||
if (item.disabled) {
|
if (item.disabled) {
|
||||||
@@ -95,6 +98,16 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
load().pipe(
|
||||||
|
Effect.tap((documents) => Effect.sync(() => (loaded.documents = documents))),
|
||||||
|
Effect.andThen(ctx.agent.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigExternalPlugin from "./external"
|
|||||||
|
|
||||||
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema, Stream } from "effect"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { fileURLToPath, pathToFileURL } from "url"
|
import { fileURLToPath, pathToFileURL } from "url"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
@@ -42,8 +42,9 @@ export const Plugin = define({
|
|||||||
const fs = yield* FSUtil.Service
|
const fs = yield* FSUtil.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const npm = yield* Npm.Service
|
const npm = yield* Npm.Service
|
||||||
yield* Effect.gen(function* () {
|
const active = new Set<string>()
|
||||||
const configured: { package: string; options?: Record<string, any> }[] = []
|
const load = Effect.fn("ConfigExternalPlugin.load")(function* () {
|
||||||
|
const configured: { package: string; options?: Record<string, unknown> }[] = []
|
||||||
|
|
||||||
for (const entry of yield* config.entries()) {
|
for (const entry of yield* config.entries()) {
|
||||||
if (entry.type === "document") {
|
if (entry.type === "document") {
|
||||||
@@ -98,8 +99,8 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ref of configured) {
|
return yield* Effect.forEach(configured, (ref) =>
|
||||||
yield* Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const entrypoint = path.isAbsolute(ref.package)
|
const entrypoint = path.isAbsolute(ref.package)
|
||||||
? pathToFileURL(ref.package).href
|
? pathToFileURL(ref.package).href
|
||||||
: (yield* npm.add(ref.package)).entrypoint
|
: (yield* npm.add(ref.package)).entrypoint
|
||||||
@@ -108,13 +109,31 @@ export const Plugin = define({
|
|||||||
const mod = yield* Effect.promise(() => import(entrypoint))
|
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||||
yield* ctx.plugin.add({
|
return {
|
||||||
id: plugin.id,
|
id: plugin.id,
|
||||||
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
effect: (host: Parameters<typeof plugin.effect>[0]) =>
|
||||||
})
|
plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||||
}).pipe(Effect.ignoreCause)
|
|
||||||
}
|
}
|
||||||
|
}).pipe(Effect.catchCause(() => Effect.succeed(undefined))),
|
||||||
|
).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
|
||||||
})
|
})
|
||||||
|
const reconcile = Effect.fn("ConfigExternalPlugin.reconcile")(function* () {
|
||||||
|
const plugins = yield* load()
|
||||||
|
const next = new Set(plugins.map((plugin) => plugin.id))
|
||||||
|
for (const id of active) {
|
||||||
|
if (!next.has(id)) yield* ctx.plugin.remove(id)
|
||||||
|
}
|
||||||
|
for (const plugin of plugins) yield* ctx.plugin.add(plugin)
|
||||||
|
active.clear()
|
||||||
|
for (const id of next) active.add(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* reconcile()
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() => reconcile()),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as ConfigProviderPlugin from "./provider"
|
export * as ConfigProviderPlugin from "./provider"
|
||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
|
|
||||||
@@ -9,14 +9,16 @@ export const Plugin = define({
|
|||||||
id: "config-provider",
|
id: "config-provider",
|
||||||
effect: Effect.fn(function* (ctx) {
|
effect: Effect.fn(function* (ctx) {
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const entries = yield* config.entries()
|
const loaded = { entries: yield* config.entries() }
|
||||||
const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
|
yield* ctx.integration.transform((integrations) => {
|
||||||
|
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
const configuredIntegrations = new Set(
|
const configuredIntegrations = new Set(
|
||||||
files.flatMap((file) =>
|
files.flatMap((file) =>
|
||||||
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
|
Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
|
||||||
|
provider.env === undefined ? [] : [id],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield* ctx.integration.transform((integrations) => {
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
for (const [id, item] of Object.entries(file.info.providers ?? {})) {
|
||||||
const integrationID = id
|
const integrationID = id
|
||||||
@@ -34,8 +36,9 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const configuredDefault = Config.latest(entries, "model")
|
|
||||||
yield* ctx.catalog.transform((catalog) => {
|
yield* ctx.catalog.transform((catalog) => {
|
||||||
|
const files = loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")
|
||||||
|
const configuredDefault = Config.latest(loaded.entries, "model")
|
||||||
if (configuredDefault !== undefined) {
|
if (configuredDefault !== undefined) {
|
||||||
const model = ModelV2.parse(configuredDefault)
|
const model = ModelV2.parse(configuredDefault)
|
||||||
catalog.model.default.set(model.providerID, model.modelID)
|
catalog.model.default.set(model.providerID, model.modelID)
|
||||||
@@ -105,5 +108,16 @@ export const Plugin = define({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.integration.reload()),
|
||||||
|
Effect.andThen(ctx.catalog.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigReferencePlugin from "./reference"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { ConfigReference } from "../reference"
|
import { ConfigReference } from "../reference"
|
||||||
import { Reference } from "../../reference"
|
import { Reference } from "../../reference"
|
||||||
@@ -16,8 +16,10 @@ export const Plugin = define({
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
|
const loaded = { entries: yield* config.entries() }
|
||||||
|
yield* ctx.reference.transform((draft) => {
|
||||||
const entries = new Map<string, Reference.Source>()
|
const entries = new Map<string, Reference.Source>()
|
||||||
for (const doc of (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")) {
|
for (const doc of loaded.entries.filter((entry): entry is Config.Document => entry.type === "document")) {
|
||||||
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||||
if (!validAlias(name)) continue
|
if (!validAlias(name)) continue
|
||||||
@@ -28,7 +30,9 @@ export const Plugin = define({
|
|||||||
local(entry)
|
local(entry)
|
||||||
? Reference.LocalSource.make({
|
? Reference.LocalSource.make({
|
||||||
type: "local",
|
type: "local",
|
||||||
path: AbsolutePath.make(localPath(directory, global.home, typeof entry === "string" ? entry : entry.path)),
|
path: AbsolutePath.make(
|
||||||
|
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||||
|
),
|
||||||
...(description === undefined ? {} : { description }),
|
...(description === undefined ? {} : { description }),
|
||||||
...(hidden === undefined ? {} : { hidden }),
|
...(hidden === undefined ? {} : { hidden }),
|
||||||
})
|
})
|
||||||
@@ -42,9 +46,18 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* ctx.reference.transform((draft) => {
|
|
||||||
for (const [name, source] of entries) draft.add(name, source)
|
for (const [name, source] of entries) draft.add(name, source)
|
||||||
})
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.reference.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as ConfigSkillPlugin from "./skill"
|
|||||||
|
|
||||||
import { define } from "../../plugin/internal"
|
import { define } from "../../plugin/internal"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect, Stream } from "effect"
|
||||||
import { Config } from "../../config"
|
import { Config } from "../../config"
|
||||||
import { AbsolutePath } from "../../schema"
|
import { AbsolutePath } from "../../schema"
|
||||||
import { SkillV2 } from "../../skill"
|
import { SkillV2 } from "../../skill"
|
||||||
@@ -15,10 +15,10 @@ export const Plugin = define({
|
|||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
const entries = yield* config.entries()
|
const loaded = { entries: yield* config.entries() }
|
||||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
|
||||||
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
|
||||||
yield* ctx.skill.transform((draft) => {
|
yield* ctx.skill.transform((draft) => {
|
||||||
|
const directories = loaded.entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||||
|
const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
draft.source(
|
draft.source(
|
||||||
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
|
||||||
@@ -44,5 +44,15 @@ export const Plugin = define({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(ctx.skill.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||||
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { CommandV2 } from "@opencode-ai/core/command"
|
||||||
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent"
|
||||||
|
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
|
||||||
|
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
|
||||||
|
import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
|
||||||
|
import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference"
|
||||||
|
import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { PluginTestLayer } from "../plugin/fixture"
|
||||||
|
|
||||||
|
const it = testEffect(PluginTestLayer)
|
||||||
|
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||||
|
const document = path.join(import.meta.dir, "opencode.json")
|
||||||
|
|
||||||
|
describe("config plugin reloads", () => {
|
||||||
|
it.live("reloads every config-backed domain", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
const catalog = yield* Catalog.Service
|
||||||
|
const commands = yield* CommandV2.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const references = yield* Reference.Service
|
||||||
|
const skills = yield* SkillV2.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
let entries: Config.Entry[] = [config("first", "First plugin")]
|
||||||
|
const service = Config.Service.of({ entries: () => Effect.sync(() => entries) })
|
||||||
|
const setup = <R>(effect: Effect.Effect<void, never, R>) =>
|
||||||
|
effect.pipe(Effect.provideService(Config.Service, service))
|
||||||
|
|
||||||
|
yield* setup(ConfigAgentPlugin.Plugin.effect(host))
|
||||||
|
yield* setup(ConfigCommandPlugin.Plugin.effect(host))
|
||||||
|
yield* setup(ConfigSkillPlugin.Plugin.effect(host))
|
||||||
|
yield* setup(ConfigReferencePlugin.Plugin.effect(host))
|
||||||
|
yield* setup(ConfigProviderPlugin.Plugin.effect(host))
|
||||||
|
yield* setup(ConfigExternalPlugin.Plugin.effect(host))
|
||||||
|
|
||||||
|
expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent")
|
||||||
|
expect((yield* commands.get("first"))?.description).toBe("First command")
|
||||||
|
expect(
|
||||||
|
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"),
|
||||||
|
).toBe(true)
|
||||||
|
expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"])
|
||||||
|
expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined()
|
||||||
|
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("First plugin")
|
||||||
|
|
||||||
|
entries = [config("second", "Second plugin")]
|
||||||
|
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||||
|
yield* waitUntil(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return (
|
||||||
|
(yield* agents.get(AgentV2.ID.make("first"))) === undefined &&
|
||||||
|
(yield* agents.get(AgentV2.ID.make("second")))?.description === "Second agent" &&
|
||||||
|
(yield* commands.get("first")) === undefined &&
|
||||||
|
(yield* commands.get("second"))?.description === "Second command" &&
|
||||||
|
(yield* references.list()).some((reference) => reference.name === "second") &&
|
||||||
|
(yield* catalog.provider.get(ProviderV2.ID.make("first"))) === undefined &&
|
||||||
|
(yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined &&
|
||||||
|
(yield* agents.get(AgentV2.ID.make("configured")))?.description === "Second plugin"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"),
|
||||||
|
).toBe(false)
|
||||||
|
expect(
|
||||||
|
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
entries = [config("second")]
|
||||||
|
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||||
|
yield* waitUntil(agents.get(AgentV2.ID.make("configured")).pipe(Effect.map((agent) => agent === undefined)))
|
||||||
|
}).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function config(name: string, pluginDescription?: string) {
|
||||||
|
return new Config.Document({
|
||||||
|
type: "document",
|
||||||
|
path: document,
|
||||||
|
info: decode({
|
||||||
|
agents: { [name]: { description: `${title(name)} agent`, mode: "subagent" } },
|
||||||
|
commands: { [name]: { template: `${title(name)} command`, description: `${title(name)} command` } },
|
||||||
|
skills: [`/skills/${name}`],
|
||||||
|
references: { [name]: `/references/${name}` },
|
||||||
|
providers: { [name]: { models: { chat: { name: `${title(name)} model` } } } },
|
||||||
|
plugins:
|
||||||
|
pluginDescription === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
package: "../plugin/fixtures/config-promise-plugin.ts",
|
||||||
|
options: { description: pluginDescription },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function title(value: string) {
|
||||||
|
return value.charAt(0).toUpperCase() + value.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
|
||||||
|
for (let attempt = 0; attempt < 100; attempt++) {
|
||||||
|
if (yield* condition) return
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
|
}
|
||||||
|
return yield* Effect.die("Timed out waiting for config plugin reloads")
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user