From b4d2da7a0964cc4a71f14bfb205400b5c8c5c1b0 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 20:01:17 +0200 Subject: [PATCH 1/2] fix(cli): preserve scoped instructions during pruning --- .changeset/protect-swe-pruner-instructions.md | 5 ++ packages/opencode/src/kilocode/swe-pruner.ts | 48 ++++++++-- .../opencode/test/kilocode/swe-pruner.test.ts | 87 +++++++++++++++++++ 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 .changeset/protect-swe-pruner-instructions.md diff --git a/.changeset/protect-swe-pruner-instructions.md b/.changeset/protect-swe-pruner-instructions.md new file mode 100644 index 00000000000..ee2388bcfd9 --- /dev/null +++ b/.changeset/protect-swe-pruner-instructions.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Preserve directory-scoped AGENTS.md instructions when SWE-Pruner filters read output. diff --git a/packages/opencode/src/kilocode/swe-pruner.ts b/packages/opencode/src/kilocode/swe-pruner.ts index 6a7d50bbba5..68ec046b302 100644 --- a/packages/opencode/src/kilocode/swe-pruner.ts +++ b/packages/opencode/src/kilocode/swe-pruner.ts @@ -32,6 +32,9 @@ const KEEP_TAIL = 5 const MERGE_GAP = 2 const MAX_KEEP_RATIO = 0.9 const TIMEOUT_MS = 15_000 +const CLOSE = "\n" +const FILE = "\nfile\n\n" +const REMINDER = `${CLOSE}\n\n\n` const DESCRIPTION = [ "Optional focus question used to prune this tool's output to only the relevant lines.", @@ -126,10 +129,27 @@ export function kept(ranges: Range[]) { return ranges.reduce((sum, [start, end]) => sum + (end - start + 1), 0) } +function partition(tool: string, result: Tool.ExecuteResult) { + if (tool !== "read") return { body: result.output, tail: "", extra: 0 } + const loaded = result.metadata["loaded"] + if (!Array.isArray(loaded) || loaded.some((item) => typeof item !== "string")) return undefined + const start = result.output.indexOf(FILE) + const index = start < 0 ? -1 : result.output.indexOf(REMINDER, start + FILE.length) + if (loaded.length === 0) return index < 0 ? { body: result.output, tail: "", extra: 0 } : undefined + if (index < 0) return undefined + const split = index + CLOSE.length + const tail = result.output.slice(split) + return { + body: result.output.slice(0, split), + tail, + extra: tail.split("\n").length - 1, + } +} + /** Reassemble the output from keep-ranges, marking omitted sections inline. */ -export function assemble(lines: string[], ranges: Range[], total: number) { +export function assemble(lines: string[], ranges: Range[], total: number, extra = 0) { const parts: string[] = [ - `[SWE-Pruner: kept ${kept(ranges)} of ${total} output lines relevant to the focus question. Omitted sections are marked below; call the tool again without ${PARAMETER} for the full output.]`, + `[SWE-Pruner: kept ${kept(ranges) + extra} of ${total + extra} output lines relevant to the focus question. Omitted sections are marked below; call the tool again without ${PARAMETER} for the full output.]`, ] let cursor = 1 for (const [start, end] of ranges) { @@ -158,7 +178,12 @@ const resolve = Effect.fn("SwePruner.resolve")(function* () { return (yield* provider.getSmallModel(ref.providerID)) ?? (yield* provider.getModel(ref.providerID, ref.modelID)) }) -const skim = Effect.fn("SwePruner.skim")(function* (input: { question: string; output: string; abort?: AbortSignal }) { +const skim = Effect.fn("SwePruner.skim")(function* (input: { + question: string + output: string + extra: number + abort?: AbortSignal +}) { const provider = yield* Provider.Service const model = yield* resolve() const language = yield* provider.getLanguage(model) @@ -190,7 +215,11 @@ const skim = Effect.fn("SwePruner.skim")(function* (input: { question: string; o if (!ranges) return undefined const keep = kept(ranges) if (keep / lines.length > MAX_KEEP_RATIO) return undefined - return { output: assemble(lines, ranges, lines.length), kept: keep, total: lines.length } + return { + output: assemble(lines, ranges, lines.length, input.extra), + kept: keep + input.extra, + total: lines.length + input.extra, + } }) /** Prune a tool result when a focus question was provided. Fails open to the original result. */ @@ -203,10 +232,13 @@ export const sweep = Effect.fn("SwePruner.sweep")(function* (input: { const focus = question(input.args) if (!focus) return input.result if (input.result.metadata["truncated"] === true) return input.result - const size = input.result.output.length + // Nearby instructions are appended to read output and must reach the main model unchanged. + const part = partition(input.tool, input.result) + if (!part) return input.result + const size = part.body.length if (size < MIN_CHARS || size > MAX_CHARS) return input.result - if (input.result.output.split("\n").length < MIN_LINES) return input.result - const pruned = yield* skim({ question: focus, output: input.result.output, abort: input.abort }).pipe( + if (part.body.split("\n").length < MIN_LINES) return input.result + const pruned = yield* skim({ question: focus, output: part.body, extra: part.extra, abort: input.abort }).pipe( Effect.catchCause((cause) => { log.error("skim failed, returning full output", { tool: input.tool, cause }) return Effect.succeed(undefined) @@ -216,7 +248,7 @@ export const sweep = Effect.fn("SwePruner.sweep")(function* (input: { log.info("pruned", { tool: input.tool, kept: pruned.kept, total: pruned.total }) return { ...input.result, - output: pruned.output, + output: pruned.output + part.tail, metadata: { ...input.result.metadata, swePruner: { question: focus, kept: pruned.kept, total: pruned.total }, diff --git a/packages/opencode/test/kilocode/swe-pruner.test.ts b/packages/opencode/test/kilocode/swe-pruner.test.ts index 396f26d4310..f3acdb8e6b0 100644 --- a/packages/opencode/test/kilocode/swe-pruner.test.ts +++ b/packages/opencode/test/kilocode/swe-pruner.test.ts @@ -1,5 +1,62 @@ import { describe, expect, test } from "bun:test" +import type { LanguageModelV3, LanguageModelV3CallOptions } from "@ai-sdk/provider" +import { Effect } from "effect" +import { Config } from "../../src/config/config" import { SwePruner } from "../../src/kilocode/swe-pruner" +import { Provider } from "../../src/provider/provider" +import { ModelID, ProviderID } from "../../src/provider/schema" + +const pid = ProviderID.make("test") +const mid = ModelID.make("swe-pruner-test") + +function model(): Provider.Model { + return { + id: mid, + providerID: pid, + api: { id: mid, npm: "test-provider", url: "" }, + limit: { context: 100_000, output: 4_000 }, + capabilities: { + toolcall: true, + attachment: false, + reasoning: false, + temperature: true, + input: { text: true, image: false, audio: false, video: false }, + output: { text: true, image: false, audio: false, video: false }, + }, + } as unknown as Provider.Model +} + +function provider(seen: string[]): Provider.Interface { + const mdl = model() + const lang = { + specificationVersion: "v3", + provider: "test", + modelId: mid, + supportedUrls: {}, + doGenerate: async (input: LanguageModelV3CallOptions) => { + seen.push(JSON.stringify(input)) + return { + content: [{ type: "text", text: "1-10" }], + finishReason: { unified: "stop" }, + usage: { + inputTokens: { total: 12 }, + outputTokens: { total: 8 }, + raw: {}, + }, + warnings: [], + providerMetadata: {}, + request: {}, + response: {}, + } + }, + } as unknown as LanguageModelV3 + return { + defaultModel: () => Effect.succeed({ providerID: pid, modelID: mid }), + getSmallModel: () => Effect.succeed(mdl), + getModel: () => Effect.succeed(mdl), + getLanguage: () => Effect.succeed(lang), + } as unknown as Provider.Interface +} describe("SwePruner.question", () => { test("extracts a non-empty focus question from raw args", () => { @@ -137,3 +194,33 @@ describe("SwePruner.kept", () => { ).toBe(6) }) }) + +describe("SwePruner.sweep", () => { + test("preserves dynamically loaded instructions outside the pruned output", async () => { + const lines = Array.from({ length: 60 }, (_, index) => `${index + 1}: ${"source content ".repeat(4)}`) + const body = `/repo/pkg/source.ts\nfile\n\n${lines.join("\n")}\n` + const rules = Array.from({ length: 10 }, (_, index) => `Keep instruction ${index + 1} intact.`) + const tail = `\n\n\nInstructions from: /repo/pkg/AGENTS.md\n${rules.join("\n")}\n` + const seen: string[] = [] + const result = await SwePruner.sweep({ + tool: "read", + args: { context_focus_question: "Where is the relevant source content?" }, + result: { + title: "source.ts", + output: body + tail, + metadata: { truncated: false, loaded: ["/repo/pkg/AGENTS.md"] }, + }, + }).pipe( + Effect.provideService(Provider.Service, provider(seen)), + Effect.provideService(Config.Service, { get: () => Effect.succeed({}) } as Config.Interface), + Effect.runPromise, + ) + + expect(seen).toHaveLength(1) + expect(seen[0]).toContain("source content") + expect(seen[0]).not.toContain(rules[0]) + expect(result.output).toEndWith(tail) + expect(result.metadata["loaded"]).toEqual(["/repo/pkg/AGENTS.md"]) + expect(result.metadata["swePruner"]).toMatchObject({ kept: 29, total: 78 }) + }) +}) From fd55804107a7ad2f5d991c5034dabdaf34b9213a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 8 Jul 2026 20:06:25 +0200 Subject: [PATCH 2/2] test(cli): cover CRLF scoped instructions --- .changeset/protect-swe-pruner-instructions.md | 2 +- packages/opencode/test/kilocode/swe-pruner.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/protect-swe-pruner-instructions.md b/.changeset/protect-swe-pruner-instructions.md index ee2388bcfd9..8bb765be72d 100644 --- a/.changeset/protect-swe-pruner-instructions.md +++ b/.changeset/protect-swe-pruner-instructions.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Preserve directory-scoped AGENTS.md instructions when SWE-Pruner filters read output. +Exclude directory-scoped AGENTS.md instructions from SWE-Pruner context. diff --git a/packages/opencode/test/kilocode/swe-pruner.test.ts b/packages/opencode/test/kilocode/swe-pruner.test.ts index f3acdb8e6b0..61bf028e16f 100644 --- a/packages/opencode/test/kilocode/swe-pruner.test.ts +++ b/packages/opencode/test/kilocode/swe-pruner.test.ts @@ -200,7 +200,7 @@ describe("SwePruner.sweep", () => { const lines = Array.from({ length: 60 }, (_, index) => `${index + 1}: ${"source content ".repeat(4)}`) const body = `/repo/pkg/source.ts\nfile\n\n${lines.join("\n")}\n` const rules = Array.from({ length: 10 }, (_, index) => `Keep instruction ${index + 1} intact.`) - const tail = `\n\n\nInstructions from: /repo/pkg/AGENTS.md\n${rules.join("\n")}\n` + const tail = `\n\n\nInstructions from: /repo/pkg/AGENTS.md\n${rules.join("\r\n")}\n` const seen: string[] = [] const result = await SwePruner.sweep({ tool: "read",