feat(core): add project reference guidance (#31601)

This commit is contained in:
Dax 2026-06-10 00:08:26 -04:00 committed by GitHub
commit 8a2cfc00c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 753 additions and 165 deletions

View file

@ -464,7 +464,9 @@ describe("Config", () => {
["@my-org/audit-plugin", { endpoint: "https://audit.example.com" }],
],
skills: { paths: ["./skills"], urls: ["https://example.com/.well-known/skills/"] },
reference: { docs: { path: "../docs" } },
references: {
docs: { path: "../docs", description: "Use for product documentation", hidden: true },
},
attachment: { image: { auto_resize: false, max_width: 1200 } },
provider: {
custom: {
@ -540,7 +542,9 @@ describe("Config", () => {
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
])
expect(documents[0]?.info.skills).toEqual(["./skills", "https://example.com/.well-known/skills/"])
expect(documents[0]?.info.references).toEqual({ docs: { path: "../docs" } })
expect(documents[0]?.info.references).toEqual({
docs: { path: "../docs", description: "Use for product documentation", hidden: true },
})
expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
expect(documents[0]?.info.providers?.custom).toMatchObject({
request: { body: { apiKey: "secret" } },

View file

@ -1,10 +1,11 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer, Schema } from "effect"
import { Effect, Equal, Hash, Layer, Schema } from "effect"
import { Tool } from "@opencode-ai/core/public"
import { Catalog } from "@opencode-ai/core/catalog"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
@ -44,6 +45,16 @@ const it = testEffect(
)
describe("LocationServiceMap", () => {
it.effect("compares equivalent location refs by value", () =>
Effect.sync(() => {
const directory = AbsolutePath.make("/project")
expect(Equal.equals(Location.Ref.make({ directory }), Location.Ref.make({ directory }))).toBe(true)
expect(Hash.hash(Location.Ref.make({ directory }))).toBe(
Hash.hash(Location.Ref.make({ directory, workspaceID: undefined })),
)
}),
)
it.live("isolates location state while sharing location policy with catalog", () =>
Effect.acquireRelease(
Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),
@ -79,7 +90,10 @@ describe("LocationServiceMap", () => {
providers: yield* catalog.provider.all(),
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
}
}).pipe(Effect.scoped, Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(directory) })))
}).pipe(
Effect.scoped,
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
)
const blockedState = yield* update(blocked.path)
expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false)

View file

@ -0,0 +1,77 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { Reference } from "@opencode-ai/core/reference"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { SystemContext } from "@opencode-ai/core/system-context/index"
import { it } from "./lib/effect"
describe("ReferenceGuidance", () => {
it.effect("lists available references in the system context", () =>
Effect.gen(function* () {
const guidance = yield* ReferenceGuidance.Service
const generation = yield* SystemContext.initialize(yield* guidance.load())
expect(generation.baseline).toContain("<available_references>")
expect(generation.baseline).toContain("<name>docs</name>")
expect(generation.baseline).toContain("<path>/docs</path>")
expect(generation.baseline).toContain("<description>Use for product documentation</description>")
}).pipe(
Effect.provide(ReferenceGuidance.layer),
Effect.provide(
Layer.mock(Reference.Service, {
list: () =>
Effect.succeed([
new Reference.Info({
name: "docs",
path: AbsolutePath.make("/docs"),
description: "Use for product documentation",
source: new Reference.LocalSource({
type: "local",
path: AbsolutePath.make("/docs"),
description: "Use for product documentation",
}),
}),
]),
}),
),
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
),
)
it.effect("omits guidance when no references are available", () =>
Effect.gen(function* () {
const guidance = yield* ReferenceGuidance.Service
const generation = yield* SystemContext.initialize(yield* guidance.load())
expect(generation.baseline).toBe("")
}).pipe(
Effect.provide(ReferenceGuidance.layer),
Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })),
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
),
)
it.effect("omits references without descriptions", () =>
Effect.gen(function* () {
const guidance = yield* ReferenceGuidance.Service
const generation = yield* SystemContext.initialize(yield* guidance.load())
expect(generation.baseline).toBe("")
}).pipe(
Effect.provide(ReferenceGuidance.layer),
Effect.provide(
Layer.mock(Reference.Service, {
list: () =>
Effect.succeed([
new Reference.Info({
name: "docs",
path: AbsolutePath.make("/docs"),
source: new Reference.LocalSource({ type: "local", path: AbsolutePath.make("/docs") }),
}),
]),
}),
),
Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })),
),
)
})

View file

@ -19,10 +19,16 @@ describe("Reference", () => {
const scope = yield* Scope.make()
const update = yield* references.transform().pipe(Effect.provideService(Scope.Scope, scope))
const path = AbsolutePath.make("/docs")
yield* update((editor) => editor.add("docs", new Reference.LocalSource({ type: "local", path })))
const source = new Reference.LocalSource({
type: "local",
path,
description: "Use for API documentation",
hidden: true,
})
yield* update((editor) => editor.add("docs", source))
expect(yield* references.list()).toEqual([
new Reference.Info({ name: "docs", path, source: new Reference.LocalSource({ type: "local", path }) }),
new Reference.Info({ name: "docs", path, description: "Use for API documentation", hidden: true, source }),
])
yield* Scope.close(scope, Exit.void)
@ -58,4 +64,33 @@ describe("Reference", () => {
Effect.provide(Global.defaultLayer),
),
)
it.effect("preserves configured Git descriptions", () =>
Effect.gen(function* () {
const references = yield* Reference.Service
const update = yield* references.transform()
const repository = Repository.parseRemote("owner/repo")
const source = new Reference.GitSource({
type: "git",
repository: "owner/repo",
description: "Use for SDK implementation details",
})
yield* update((editor) => editor.add("sdk", source))
expect(yield* references.list()).toEqual([
new Reference.Info({
name: "sdk",
path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)),
description: "Use for SDK implementation details",
source,
}),
])
}).pipe(
Effect.scoped,
Effect.provide(Reference.layer),
Effect.provide(cache),
Effect.provide(EventV2.defaultLayer),
Effect.provide(Global.defaultLayer),
),
)
})

View file

@ -10,7 +10,27 @@ import { testEffect } from "./lib/effect"
const it = testEffect(Ripgrep.defaultLayer)
describe("Ripgrep", () => {
it.live("allows caller globs to re-include git metadata", () =>
it.live("keeps ignored files out of catch-all find results", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true }))
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`)
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n"))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n"))
yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n"))
const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 })
expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js"))
expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js"))
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("never includes git metadata", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) =>
@ -23,7 +43,7 @@ describe("Ripgrep", () => {
const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 })
expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config"))
expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config"))
const observed: string[] = []
const limited = yield* ripgrep.find({
@ -36,7 +56,7 @@ describe("Ripgrep", () => {
const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config"))
expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config"))
}),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),

View file

@ -25,6 +25,7 @@ import { Location } from "@opencode-ai/core/location"
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { describe, expect } from "bun:test"
import { eq } from "drizzle-orm"
import { Effect, Layer } from "effect"
@ -70,6 +71,7 @@ const models = SessionRunnerModel.layerWith(() => Effect.succeed(model))
const systemContext = SystemContextRegistry.layer
const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
const runner = SessionRunnerLLM.defaultLayer.pipe(
Layer.provide(database),
@ -82,6 +84,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
Layer.provide(location),
Layer.provide(agents),
Layer.provide(skillGuidance),
Layer.provide(referenceGuidance),
Layer.provide(config),
)
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))

View file

@ -48,6 +48,7 @@ import { SessionStore } from "@opencode-ai/core/session/store"
import { SystemContext } from "@opencode-ai/core/system-context"
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
import { ModelV2 } from "@opencode-ai/core/model"
import { Location } from "@opencode-ai/core/location"
import { ProviderV2 } from "@opencode-ai/core/provider"
@ -215,6 +216,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
: SystemContext.empty,
),
})
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
const config = Layer.succeed(
Config.Service,
Config.Service.of({
@ -243,6 +245,7 @@ const runner = SessionRunnerLLM.layer.pipe(
Layer.provide(location),
Layer.provide(agents),
Layer.provide(skillGuidance),
Layer.provide(referenceGuidance),
Layer.provide(config),
)
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))