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
|
|
@ -24,9 +24,13 @@ import { Effect, Context, Layer, Schema } from "effect"
|
|||
import { InstanceState } from "@/effect/instance-state"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
name: Schema.String,
|
||||
|
|
@ -89,15 +93,21 @@ export const layer = Layer.effect(
|
|||
const plugin = yield* Plugin.Service
|
||||
const skill = yield* Skill.Service
|
||||
const provider = yield* Provider.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("Agent.state")(function* (ctx) {
|
||||
const cfg = yield* config.get()
|
||||
const skillDirs = yield* skill.dirs()
|
||||
const referenceDirs = yield* Effect.gen(function* () {
|
||||
yield* (yield* PluginBoot.Service).wait()
|
||||
return (yield* (yield* Reference.Service).list()).map((reference) => reference.path)
|
||||
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
||||
const whitelistedDirs = [
|
||||
Truncate.GLOB,
|
||||
path.join(Global.Path.tmp, "*"),
|
||||
...skillDirs.map((dir) => path.join(dir, "*")),
|
||||
...referenceDirs.map((dir) => path.join(dir, "*")),
|
||||
]
|
||||
const readonlyExternalDirectory = {
|
||||
"*": "ask",
|
||||
|
|
@ -429,8 +439,18 @@ export const defaultLayer = layer.pipe(
|
|||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
)
|
||||
|
||||
export const node = LayerNode.make(layer, [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node])
|
||||
const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, [])
|
||||
|
||||
export const node = LayerNode.make(layer, [
|
||||
Config.node,
|
||||
Auth.node,
|
||||
Plugin.node,
|
||||
Skill.node,
|
||||
Provider.node,
|
||||
locationServiceMapNode,
|
||||
])
|
||||
|
||||
export * as Agent from "./agent"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import { EOL } from "os"
|
|||
import { Effect } from "effect"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||
effect.pipe(
|
||||
Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })),
|
||||
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { EOL } from "os"
|
|||
import { Effect, Option } from "effect"
|
||||
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 { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
|
|
@ -37,9 +38,11 @@ export const V2Command = effectCmd({
|
|||
}).pipe(
|
||||
Effect.withSpan("Cli.debug.v2"),
|
||||
Effect.provide(
|
||||
LocationServiceMap.get({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
}),
|
||||
LocationServiceMap.get(
|
||||
Location.Ref.make({
|
||||
directory: AbsolutePath.make(process.cwd()),
|
||||
}),
|
||||
),
|
||||
),
|
||||
Effect.provide(LocationServiceMap.layer),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
|
|||
|
||||
const filesystem = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { handlePtyInput } from "@opencode-ai/core/pty/input"
|
|||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
|
|
@ -41,13 +42,15 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
|
|||
const cors = yield* CorsConfig
|
||||
const locations = yield* LocationServiceMap
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unregister))
|
||||
|
||||
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -158,13 +161,15 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
|
|||
const cors = yield* CorsConfig
|
||||
const locations = yield* LocationServiceMap
|
||||
const unregister = registerDisposer((directory) =>
|
||||
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
|
||||
Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unregister))
|
||||
|
||||
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
Effect.provide(
|
||||
locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ import type { Provider } from "@/provider/provider"
|
|||
import type { Agent } from "@/agent/agent"
|
||||
import { Permission } from "@/permission"
|
||||
import { Skill } from "@/skill"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
|
||||
export function provider(model: Provider.Model) {
|
||||
if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
||||
|
|
@ -44,10 +49,15 @@ export const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const skill = yield* Skill.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
return Service.of({
|
||||
environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const references = yield* Effect.gen(function* () {
|
||||
yield* (yield* PluginBoot.Service).wait()
|
||||
return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined)
|
||||
}).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) }))))
|
||||
return [
|
||||
[
|
||||
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
|
||||
|
|
@ -60,7 +70,25 @@ export const layer = Layer.effect(
|
|||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
].join("\n"),
|
||||
]
|
||||
references.length === 0
|
||||
? undefined
|
||||
: [
|
||||
"Project references provide additional directories that can be accessed when relevant.",
|
||||
"<available_references>",
|
||||
...references
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.flatMap((reference) => [
|
||||
" <reference>",
|
||||
` <name>${reference.name}</name>`,
|
||||
` <path>${reference.path}</path>`,
|
||||
...(reference.description === undefined
|
||||
? []
|
||||
: [` <description>${reference.description}</description>`]),
|
||||
" </reference>",
|
||||
]),
|
||||
"</available_references>",
|
||||
].join("\n"),
|
||||
].filter((part): part is string => part !== undefined)
|
||||
}),
|
||||
|
||||
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
|
||||
|
|
@ -80,8 +108,10 @@ export const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer), Layer.provide(LocationServiceMap.layer))
|
||||
|
||||
export const node = LayerNode.make(layer, [Skill.node])
|
||||
const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, [])
|
||||
|
||||
export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode])
|
||||
|
||||
export * as SystemPrompt from "./system"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { Plugin } from "../../src/plugin"
|
|||
import { Provider } from "../../src/provider/provider"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { Truncate } from "../../src/tool/truncate"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
|
||||
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
||||
Agent.layer.pipe(
|
||||
|
|
@ -22,6 +23,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
|
|||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(Skill.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
)
|
||||
|
||||
|
|
@ -119,7 +121,7 @@ it.instance(
|
|||
}),
|
||||
{
|
||||
config: {
|
||||
reference: {
|
||||
references: {
|
||||
effect: "github.com/effect/effect-smol",
|
||||
effectFull: {
|
||||
repository: "Effect-TS/effect",
|
||||
|
|
@ -601,6 +603,25 @@ description: Permission skill.
|
|||
{ git: true },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"project reference directories are allowed for external_directory",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const build = yield* load((svc) => svc.get("build"))
|
||||
const target = path.resolve(test.directory, "../docs/reference/notes.md")
|
||||
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
||||
}),
|
||||
{
|
||||
git: true,
|
||||
config: {
|
||||
references: {
|
||||
docs: "../docs",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
it.instance("defaultAgent returns build when no default_agent config", () =>
|
||||
Effect.gen(function* () {
|
||||
const agent = yield* load((svc) => svc.defaultAgent())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect } from "bun:test"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import path from "path"
|
||||
|
|
@ -43,6 +44,7 @@ const agentLayer = Agent.layer.pipe(
|
|||
Layer.provide(SkillTest.empty),
|
||||
Layer.provide(provider.layer),
|
||||
Layer.provide(pluginLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ describe("reference HttpApi", () => {
|
|||
config: {
|
||||
formatter: false,
|
||||
lsp: false,
|
||||
reference: {
|
||||
references: {
|
||||
docs: "./docs",
|
||||
effect: { repository: "Effect-TS/effect", branch: "main" },
|
||||
bad: "not-a-repo",
|
||||
|
|
@ -35,18 +35,26 @@ describe("reference HttpApi", () => {
|
|||
{
|
||||
name: "docs",
|
||||
path: path.join(tmp.path, "docs"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
source: {
|
||||
type: "local",
|
||||
path: path.join(tmp.path, "docs"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "effect",
|
||||
path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"),
|
||||
description: null,
|
||||
hidden: null,
|
||||
source: {
|
||||
type: "git",
|
||||
repository: "Effect-TS/effect",
|
||||
branch: "main",
|
||||
description: null,
|
||||
hidden: null,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error"
|
|||
import { Skill } from "../../src/skill"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const skills: Skill.Info[] = [
|
||||
|
|
@ -42,6 +43,7 @@ const build: Agent.Info = {
|
|||
|
||||
const it = testEffect(
|
||||
SystemPrompt.layer.pipe(
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(
|
||||
Layer.succeed(
|
||||
Skill.Service,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue