refactor(scout): resolve configured reference mentions (#26701)

This commit is contained in:
Shoubhit Dash
2026-05-11 12:57:26 +05:30
committed by GitHub
parent 5d6f2a1524
commit 7e997cfba4
9 changed files with 304 additions and 116 deletions

View File

@@ -141,16 +141,12 @@ test("scout agent allows repo cloning and repo cache reads", async () => {
})
})
test("reference config creates scout-backed subagents", async () => {
test("reference config does not create subagents", async () => {
await withExperimentalScout(true, async () => {
await using tmp = await tmpdir({
config: {
reference: {
effect: "github.com/effect/effect-smol",
effectDev: {
repository: "https://github.com/effect/effect-smol",
branch: "dev",
},
effectFull: {
repository: "Effect-TS/effect",
branch: "main",
@@ -165,45 +161,13 @@ test("reference config creates scout-backed subagents", async () => {
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const effect = await load(tmp.path, (svc) => svc.get("effect"))
const effectDev = await load(tmp.path, (svc) => svc.get("effectDev"))
const effectFull = await load(tmp.path, (svc) => svc.get("effectFull"))
const local = await load(tmp.path, (svc) => svc.get("localdocs"))
const localFull = await load(tmp.path, (svc) => svc.get("localdocsFull"))
expect(effect).toBeDefined()
expect(effect?.mode).toBe("subagent")
expect(effect?.prompt).toContain("Repository: github.com/effect/effect-smol")
expect(effect?.prompt).toContain(
`Cached directory: ${path.join(Global.Path.repos, "github.com", "effect", "effect-smol")}`,
)
expect(effect?.prompt).toContain("Do not call repo_clone")
expect(evalPerm(effect, "repo_clone")).toBe("deny")
expect(effectDev).toBeDefined()
expect(effectDev?.prompt).toContain("Problem: Reference conflicts with @effect")
expect(effectDev?.prompt).not.toContain("Cached directory:")
expect(effectFull).toBeDefined()
expect(effectFull?.mode).toBe("subagent")
expect(effectFull?.prompt).toContain("Repository: Effect-TS/effect")
expect(effectFull?.prompt).toContain("Branch/ref: main")
expect(evalPerm(effectFull, "repo_clone")).toBe("deny")
expect(local).toBeDefined()
expect(local?.mode).toBe("subagent")
expect(local?.prompt).toContain(`Local directory: ${path.resolve(tmp.path, "../docs")}`)
expect(
Permission.evaluate(
"external_directory",
path.join(path.resolve(tmp.path, "../docs"), "README.md"),
local!.permission,
).action,
).toBe("allow")
expect(localFull).toBeDefined()
expect(localFull?.mode).toBe("subagent")
expect(localFull?.prompt).toContain(`Local directory: ${path.resolve(tmp.path, "../local-docs")}`)
const agents = await load(tmp.path, (svc) => svc.list())
const names = agents.map((agent) => agent.name)
expect(names).toContain("scout")
expect(names).not.toContain("effect")
expect(names).not.toContain("effectFull")
expect(names).not.toContain("localdocs")
expect(names).not.toContain("localdocsFull")
},
})
})

View File

@@ -2,6 +2,7 @@ import { NodeFileSystem } from "@effect/platform-node"
import { FetchHttpClient } from "effect/unstable/http"
import { expect } from "bun:test"
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import fs from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
import { NamedError } from "@opencode-ai/core/util/error"
@@ -203,6 +204,7 @@ function makeHttp() {
SessionPrompt.layer.pipe(
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(Reference.defaultLayer),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
@@ -1791,6 +1793,101 @@ it.live("keeps stored part order stable when file resolution is async", () =>
),
)
it.live("resolves configured reference mentions before workspace paths and agents", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const docs = path.join(dir, "external-docs")
yield* Effect.promise(() => fs.mkdir(path.join(docs, "guide"), { recursive: true }))
yield* Effect.promise(() => fs.mkdir(path.join(dir, "docs"), { recursive: true }))
yield* Effect.promise(() => Bun.write(path.join(docs, "README.md"), "reference readme"))
yield* Effect.promise(() => Bun.write(path.join(docs, "guide", "intro.md"), "reference intro"))
yield* Effect.promise(() => Bun.write(path.join(dir, "docs", "README.md"), "workspace readme"))
const prompt = yield* SessionPrompt.Service
const parts = yield* prompt.resolvePromptParts(
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
)
const references = parts.filter(
(part): part is MessageV2.TextPartInput =>
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
)
const files = parts.filter((part): part is MessageV2.FilePartInput => part.type === "file")
const agents = parts.filter((part): part is MessageV2.AgentPartInput => part.type === "agent")
const bare = references.find((part) => part.text.includes("@docs."))
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
const guide = files.find((part) => part.filename === "docs/guide")
expect(references.length).toBe(2)
expect(bare?.metadata?.reference).toMatchObject({
name: "docs",
kind: "local",
path: docs,
})
expect(missing?.text).toContain("Path does not exist inside configured reference @docs")
expect(missing?.metadata?.reference).toMatchObject({
target: "missing.md",
targetPath: path.join(docs, "missing.md"),
})
expect(files.length).toBe(2)
expect(files.map((file) => fileURLToPath(file.url)).sort()).toEqual(
[path.join(docs, "README.md"), path.join(docs, "guide")].sort(),
)
expect(guide?.mime).toBe("application/x-directory")
expect(agents.map((agent) => agent.name)).toEqual(["build"])
}),
{
git: true,
config: {
...cfg,
reference: {
docs: "./external-docs",
},
},
},
),
)
it.live("injects metadata for bare configured reference mentions", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const docs = path.join(dir, "external-docs")
yield* Effect.promise(() => fs.mkdir(docs, { recursive: true }))
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})
const message = yield* prompt.prompt({
sessionID: session.id,
noReply: true,
parts: yield* prompt.resolvePromptParts("Use @docs for context"),
})
const stored = MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts
.filter((part): part is MessageV2.TextPart => part.type === "text" && part.synthetic === true)
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
expect(reference?.metadata?.reference).toMatchObject({ name: "docs", kind: "local", path: docs })
expect(synthetic.some((part) => part.text.includes(`Reference root: ${docs}`))).toBe(true)
expect(synthetic.some((part) => part.text.includes("subagent scout"))).toBe(true)
yield* sessions.remove(session.id)
}),
{
git: true,
config: {
...cfg,
reference: {
docs: "./external-docs",
},
},
},
),
)
// Special characters in filenames
it.live("handles filenames with # character", () =>

View File

@@ -154,6 +154,7 @@ function makeHttp() {
SessionPrompt.layer.pipe(
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(Reference.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provideMerge(run),
Layer.provideMerge(compact),