feat(core): make path-local instruction discovery durable
Replace synthetic-message instruction injection with a durable discovery projection so discovered AGENTS.md files survive compaction, forks, reverts, and restarts. - Add the instruction_file table and the durable session.instructions.discovered event; projection stores each discovered path and content with its owning assistant-message boundary and durable discovery order. - Fold discovered files into the core/instructions source through InstructionDiscovery, absorbing SessionInstructions. Completed compaction rebaselines restate discovered instructions instead of summarizing them away. - Re-read discovered files live at each observation so mid-session edits reach the model; the frozen discovery content stands in only when a file becomes unreadable. - Narrate instruction file changes as per-file deltas via diffByKey, restating the full set only for pure reorderings. - Fork copies discoveries within the inherited transcript, committed revert removes discoveries past the revert boundary, and Session movement clears them so the destination initializes a complete baseline.
This commit is contained in:
parent
754dd57177
commit
0726f09b05
28 changed files with 1166 additions and 598 deletions
|
|
@ -20,7 +20,8 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
|||
import { ReadTool } from "@opencode-ai/core/tool/read"
|
||||
import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
|
||||
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
|
||||
import { SessionInstructions } from "@opencode-ai/core/session/instructions"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { Instructions } from "@opencode-ai/core/instructions"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool"
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ const readToolNode = makeLocationNode({
|
|||
LocationMutation.node,
|
||||
Image.node,
|
||||
PermissionV2.node,
|
||||
SessionInstructions.node,
|
||||
InstructionDiscovery.node,
|
||||
FSUtil.node,
|
||||
Location.node,
|
||||
],
|
||||
|
|
@ -47,6 +48,8 @@ const readCalls: {
|
|||
page: ReadToolFileSystem.PageInput
|
||||
}[] = []
|
||||
const listCalls: ReadToolFileSystem.PageInput[] = []
|
||||
const discoveredInstructions: string[][] = []
|
||||
let instructionPaths: string[] = []
|
||||
let resolvedType: "file" | "directory" = "file"
|
||||
let resolveFailure: unknown
|
||||
let readResult: FileSystem.Content | ReadToolFileSystem.TextPage = {
|
||||
|
|
@ -108,6 +111,7 @@ const testFileSystem = Layer.effect(
|
|||
}),
|
||||
)
|
||||
: Effect.succeed(path),
|
||||
up: () => Effect.succeed(instructionPaths),
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -146,6 +150,10 @@ const unavailableImage = Layer.succeed(
|
|||
Image.Service,
|
||||
Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }),
|
||||
)
|
||||
const instructionDiscovery = Layer.mock(InstructionDiscovery.Service, {
|
||||
load: (_sessionID) => Effect.succeed(Instructions.empty),
|
||||
discover: (input) => Effect.sync(() => void discoveredInstructions.push([...input.paths])),
|
||||
})
|
||||
const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
||||
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, readToolNode]), [
|
||||
[ReadToolFileSystem.node, reader],
|
||||
|
|
@ -155,6 +163,7 @@ const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
|||
[LocationMutation.node, mutation],
|
||||
[FSUtil.node, testFileSystem],
|
||||
[Location.node, locationLayer],
|
||||
[InstructionDiscovery.node, instructionDiscovery],
|
||||
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
||||
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
|
||||
])
|
||||
|
|
@ -167,6 +176,8 @@ describe("ReadTool", () => {
|
|||
assertions.length = 0
|
||||
readCalls.length = 0
|
||||
listCalls.length = 0
|
||||
discoveredInstructions.length = 0
|
||||
instructionPaths = []
|
||||
allow = true
|
||||
resolvedType = "file"
|
||||
resolveFailure = undefined
|
||||
|
|
@ -678,6 +689,7 @@ describe("ReadTool", () => {
|
|||
mime: "application/octet-stream",
|
||||
}
|
||||
const registry = yield* ToolRegistry.Service
|
||||
instructionPaths = [path.join(process.cwd(), "sub", "AGENTS.md")]
|
||||
|
||||
expect(
|
||||
yield* executeTool(registry, {
|
||||
|
|
@ -686,6 +698,7 @@ describe("ReadTool", () => {
|
|||
call: { type: "tool-call", id: "call-direct-binary", name: "read", input: { path: "late-binary" } },
|
||||
}),
|
||||
).toEqual({ type: "error", value: "Cannot read binary file: late-binary" })
|
||||
expect(discoveredInstructions).toEqual([])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue