Compare commits

..

1 Commits

Author SHA1 Message Date
Kit Langton 93bfa34b57 fix(core): discover local plugin packages 2026-08-11 12:08:04 -04:00
4 changed files with 108 additions and 61 deletions
@@ -348,10 +348,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue
}
}
const previous = messages.at(-1)
if (previous?.role === "user")
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
messages.push({ role: "user", content })
continue
}
@@ -395,10 +392,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
const cachePoint = BedrockCache.block(breakpoints, part.cache)
if (cachePoint) content.push(cachePoint)
}
const previous = messages.at(-1)
if (previous?.role === "user")
messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
else messages.push({ role: "user", content })
messages.push({ role: "user", content })
}
return messages
@@ -255,57 +255,6 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("merges parallel tool results into one user message", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
id: "req_parallel_history",
model,
messages: [
Message.user("Compare the weather."),
Message.assistant([
ToolCallPart.make({ id: "tool_paris", name: "lookup", input: { city: "Paris" } }),
ToolCallPart.make({ id: "tool_london", name: "lookup", input: { city: "London" } }),
]),
Message.tool({ id: "tool_paris", name: "lookup", result: { forecast: "sunny" } }),
Message.tool({ id: "tool_london", name: "lookup", result: { forecast: "rainy" } }),
],
cache: "none",
}),
)
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ text: "Compare the weather." }] },
{
role: "assistant",
content: [
{ toolUse: { toolUseId: "tool_paris", name: "lookup", input: { city: "Paris" } } },
{ toolUse: { toolUseId: "tool_london", name: "lookup", input: { city: "London" } } },
],
},
{
role: "user",
content: [
{
toolResult: {
toolUseId: "tool_paris",
content: [{ json: { forecast: "sunny" } }],
status: "success",
},
},
{
toolResult: {
toolUseId: "tool_london",
content: [{ json: { forecast: "rainy" } }],
status: "success",
},
},
],
},
])
}),
)
it.effect("lowers image content in tool-result messages", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
+39 -2
View File
@@ -4,7 +4,7 @@ import { Directory, Document, type Entry } from "@opencode-ai/schema/config"
import { ConfigPlugin } from "@opencode-ai/schema/config/plugin"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Context, Effect, Layer, Option, PubSub, Scope, Stream } from "effect"
import { Context, Effect, Layer, Option, PubSub, Schema, Scope, Stream } from "effect"
import path from "path"
import { fileURLToPath } from "url"
import { Config } from "../../config"
@@ -154,6 +154,12 @@ const scan = Effect.fn("ConfigPluginSource.scan")(function* (
})
const sourceDirectories = ["plugin", "plugins"] as const
const Package = Schema.Struct({
exports: Schema.optional(Schema.Unknown),
module: Schema.optional(Schema.Unknown),
main: Schema.optional(Schema.Unknown),
})
const decodePackage = Schema.decodeUnknownOption(Package)
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
return Effect.gen(function* () {
@@ -166,7 +172,38 @@ function discoverDirectory(fs: FSUtil.Interface, directory: string) {
symlink: true,
})
.pipe(Effect.orElseSucceed(() => []))
return files.sort().map((target): Operation => ({ type: "add", target, options: {} }))
const children = yield* fs
.scan(`{${sourceDirectories.join(",")}}/*`, {
cwd: directory,
absolute: true,
include: "all",
dot: true,
symlink: true,
})
.pipe(Effect.orElseSucceed(() => []))
const directories = yield* Effect.filter(children.sort(), fs.isDir)
const packages = yield* Effect.forEach(directories, (child) => discoverPackage(fs, child))
return [...files.sort(), ...packages.filter((target): target is string => typeof target === "string")].map(
(target): Operation => ({ type: "add", target, options: {} }),
)
})
}
function discoverPackage(fs: FSUtil.Interface, directory: string) {
return Effect.gen(function* () {
const manifest = yield* fs
.readJson(path.join(directory, "package.json"))
.pipe(Effect.map(decodePackage), Effect.orElseSucceed(Option.none))
const configured = Option.isSome(manifest)
? [manifest.value.exports, manifest.value.module, manifest.value.main].filter(
(entry): entry is string => typeof entry === "string",
)
: []
const target = yield* Effect.findFirst(
[...configured, "index.ts", "index.js"].map((entry) => path.resolve(directory, entry)),
fs.isFile,
)
return Option.getOrUndefined(target)
})
}
+67
View File
@@ -168,6 +168,69 @@ describe("PluginSupervisor config", () => {
),
)
it.live("loads auto-discovered plugin packages from package metadata", () =>
withLocation(
undefined,
Effect.gen(function* () {
yield* ready()
const plugins = yield* Plugin.Service
expect((yield* plugins.list()).map((plugin) => String(plugin.id))).toContain("package-metadata")
}),
false,
async (directory) => {
const plugin = path.join(directory, ".opencode", "plugins", "package-metadata")
await fs.mkdir(plugin, { recursive: true })
await fs.writeFile(path.join(plugin, "package.json"), JSON.stringify({ exports: "./entry.ts" }))
await fs.writeFile(path.join(plugin, "entry.ts"), discoveredPlugin("package-metadata"))
},
),
)
it.live("loads auto-discovered plugin packages from index fallback", () =>
withLocation(
undefined,
Effect.gen(function* () {
yield* ready()
const plugins = yield* Plugin.Service
expect((yield* plugins.list()).map((plugin) => String(plugin.id))).toContain("index-fallback")
}),
false,
async (directory) => {
const plugin = path.join(directory, ".opencode", "plugins", "index-fallback")
await fs.mkdir(plugin, { recursive: true })
await fs.writeFile(path.join(plugin, "index.js"), discoveredPlugin("index-fallback"))
},
),
)
it.live("prefers package metadata over index fallback", () =>
withLocation(
undefined,
Effect.gen(function* () {
yield* ready()
const plugins = yield* Plugin.Service
const ids = (yield* plugins.list()).map((plugin) => String(plugin.id))
expect(ids).toContain("metadata-precedence")
expect(ids).not.toContain("module-collision")
expect(ids).not.toContain("main-collision")
expect(ids).not.toContain("index-collision")
}),
false,
async (directory) => {
const plugin = path.join(directory, ".opencode", "plugins", "collision")
await fs.mkdir(plugin, { recursive: true })
await fs.writeFile(
path.join(plugin, "package.json"),
JSON.stringify({ exports: "./entry.js", module: "./module.js", main: "./main.js" }),
)
await fs.writeFile(path.join(plugin, "entry.js"), discoveredPlugin("metadata-precedence"))
await fs.writeFile(path.join(plugin, "module.js"), discoveredPlugin("module-collision"))
await fs.writeFile(path.join(plugin, "main.js"), discoveredPlugin("main-collision"))
await fs.writeFile(path.join(plugin, "index.js"), discoveredPlugin("index-collision"))
},
),
)
staticIt.live("uses only internal and SDK plugins when the static source is wired", () =>
Effect.gen(function* () {
const sdk = yield* SdkPlugins.Service
@@ -389,3 +452,7 @@ export default Plugin.define({
})
`
}
function discoveredPlugin(id: string) {
return `export default { id: ${JSON.stringify(id)}, setup() {} }`
}