feat(core): add project reference guidance (#31601)
This commit is contained in:
parent
0fc33e2a06
commit
8a2cfc00c9
38 changed files with 753 additions and 165 deletions
|
|
@ -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" } },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
77
packages/core/test/reference-guidance.test.ts
Normal file
77
packages/core/test/reference-guidance.test.ts
Normal 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 })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue