fix(core): deduplicate direct instruction reads
This commit is contained in:
parent
debdea40ea
commit
0ed6a9c1a9
3 changed files with 43 additions and 15 deletions
|
|
@ -20,7 +20,7 @@ export interface Interface {
|
|||
readonly load: (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly paths: ReadonlyArray<string>
|
||||
}) => Effect.Effect<void, MessageDecodeError | FSUtil.Error>
|
||||
}) => Effect.Effect<ReadonlyArray<string>, MessageDecodeError | FSUtil.Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionInstructions") {}
|
||||
|
|
@ -53,10 +53,10 @@ const layer = Layer.effect(
|
|||
next.set(input.sessionID, new Set([...existing, ...newlyClaimed]))
|
||||
return [newlyClaimed, next]
|
||||
})
|
||||
if (claimed.length === 0) return
|
||||
if (claimed.length === 0) return []
|
||||
const alreadyInjected = yield* previouslyInjected(store, input.sessionID)
|
||||
const toInject = claimed.filter((path) => !alreadyInjected.has(path))
|
||||
if (toInject.length === 0) return
|
||||
if (toInject.length === 0) return []
|
||||
const files = yield* Effect.forEach(
|
||||
toInject,
|
||||
(path) =>
|
||||
|
|
@ -66,7 +66,8 @@ const layer = Layer.effect(
|
|||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const readable = files.filter((file): file is { path: string; content: string } => file !== undefined)
|
||||
if (readable.length === 0) return
|
||||
if (readable.length === 0) return []
|
||||
const paths = readable.map((file) => file.path)
|
||||
// Publish directly rather than through Session.synthetic: a Location-scoped layer
|
||||
// cannot depend on Session (it routes through LocationServiceMap, forming a type
|
||||
// cycle with this node). The durable publish is what makes the synthetic visible on
|
||||
|
|
@ -76,8 +77,9 @@ const layer = Layer.effect(
|
|||
sessionID: input.sessionID,
|
||||
text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"),
|
||||
description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`,
|
||||
metadata: { instruction: { paths: readable.map((file) => file.path) } },
|
||||
metadata: { instruction: { paths } },
|
||||
})
|
||||
return paths
|
||||
})
|
||||
|
||||
return Service.of({ load })
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ export const Plugin = {
|
|||
// directory listing the walk starts at the directory itself (so its own AGENTS.md
|
||||
// is discovered); for a file it starts at the file's dirname. External reads are
|
||||
// skipped, and discovery failures never fail the read.
|
||||
yield* Effect.gen(function* () {
|
||||
if (target.externalDirectory !== undefined) return
|
||||
const instructions = yield* Effect.gen(function* () {
|
||||
if (target.externalDirectory !== undefined) return [] as string[]
|
||||
const resolved = yield* fs.resolve(target.canonical)
|
||||
const root = yield* fs.resolve(location.directory)
|
||||
// up() searches its stop directory, so the Location-root AGENTS.md (already
|
||||
|
|
@ -106,19 +106,20 @@ export const Plugin = {
|
|||
const candidates = (yield* Effect.forEach(discovered, fs.resolve)).filter(
|
||||
(file) => dirname(file) !== root,
|
||||
)
|
||||
if (candidates.length === 0) return
|
||||
yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })
|
||||
return yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })
|
||||
}).pipe(
|
||||
Effect.catch(() => Effect.void),
|
||||
Effect.catchDefect(() => Effect.void),
|
||||
Effect.catch(() => Effect.succeed([] as string[])),
|
||||
Effect.catchDefect(() => Effect.succeed([] as string[])),
|
||||
)
|
||||
if (content.type === "file" && content.encoding === "base64" && !SUPPORTED_MEDIA_MIMES.has(content.mime))
|
||||
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource }))
|
||||
return content
|
||||
return { output: content, instructions, absolute }
|
||||
}).pipe(
|
||||
Effect.map((output) => ({
|
||||
output,
|
||||
content: toModelContent(input.path, input.offset, output),
|
||||
Effect.map((result) => ({
|
||||
output: result.output,
|
||||
content: result.instructions.includes(result.absolute)
|
||||
? `Read ${input.path}; its full contents are loaded as session instructions.`
|
||||
: toModelContent(input.path, input.offset, result.output),
|
||||
})),
|
||||
Effect.mapError((error) => {
|
||||
if (error instanceof ToolFailure) return error
|
||||
|
|
|
|||
|
|
@ -272,6 +272,31 @@ describe("SessionInstructions", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("returns a receipt when a direct AGENTS.md read injects the same content", () =>
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const dir = location.directory
|
||||
const subPath = path.resolve(dir, "sub", "AGENTS.md")
|
||||
yield* mkdir(path.dirname(subPath))
|
||||
yield* writeAgents(subPath, "sub-instructions")
|
||||
|
||||
const session = yield* Session.Service
|
||||
const registry = yield* Tool.Service
|
||||
const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id
|
||||
const result = yield* executeTool(registry, readCall(sessionID, "call-agents", "sub/AGENTS.md"))
|
||||
|
||||
expect(result.status).toBe("completed")
|
||||
if (result.status !== "completed") return
|
||||
expect(result.output.content).toBe("sub-instructions")
|
||||
expect(result.content).toEqual([
|
||||
{ type: "text", text: "Read sub/AGENTS.md; its full contents are loaded as session instructions." },
|
||||
])
|
||||
expect((yield* synthetics(sessionID)).map((message) => message.text)).toEqual([
|
||||
`Instructions from: ${subPath}\nsub-instructions`,
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("loads instructions directly without a read", () =>
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue