Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 +1438,12 @@ export const layer = Layer.effect(
instruction.system().pipe(Effect.orDie),
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
// Cache-friendly order: place stable instructions/skills first so byte 0 of
// the assembled system payload is invariant across sessions, days, and users.
// The env block (which contains a per-session datetime, per-project cwd, and
// per-agent model id) goes at the tail so it cannot defeat prefix-cache hits
// on the upstream model. See anomalyco/opencode#20110, anomalyco/opencode#5224.
const system = [...instructions, ...(skills ? [skills] : []), ...env]
const format = lastUser.format ?? { type: "text" as const }
if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
const result = yield* handle.process({
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const layer = Layer.effect(
const ctx = yield* InstanceState.context
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`\nYou are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${ctx.directory}`,
Expand Down
44 changes: 43 additions & 1 deletion packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "fs"
import { NodeFileSystem } from "@effect/platform-node"
import { FetchHttpClient } from "effect/unstable/http"
import { expect } from "bun:test"
import { expect, test } from "bun:test"
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
Expand Down Expand Up @@ -2343,3 +2344,44 @@ noLLMServer.instance(
}),
30_000,
)

test("session/prompt: source assembles system array with env at tail for cache stability", () => {
// Enforce that the assembled `system` array in runLoop puts the volatile
// env block (cwd, datetime, platform, model id) at the END so byte 0 of
// the system payload is invariant across sessions, days, and users.
// This enables exact-prefix cache reuse on every provider opencode targets.
// Related: anomalyco/opencode#20110, anomalyco/opencode#5224.
const source = readFileSync(
path.resolve(__dirname, "../../src/session/prompt.ts"),
"utf8",
)
const match = source.match(
/const system = \[\.\.\.(\w+),\s*\.\.\.\(skills \? \[skills\] : \[\]\),\s*\.\.\.(\w+)\]/,
)
expect(match).not.toBeNull()
expect(match![1]).toBe("instructions")
expect(match![2]).toBe("env")
})

test("session/prompt: env block leads with \\n so the skills/env seam is a blank line", () => {
// The env block string returned by SystemPrompt.environment() must start
// with "\n" so that when request.ts joins the system array with "\n", the
// seam between </available_skills> and the env preamble becomes "\n\n"
// (a blank line). This makes the byte sequence at the boundary canonical
// and stable, enabling downstream prefix-cache hit rate to match what the
// LiteLLM opencode_reorder hook previously achieved server-side.
//
// We assert on the source of system.ts: the template-literal that opens
// the environment block must start with "\n" (a leading newline).
// Related: anomalyco/opencode#20110, anomalyco/opencode#5224.
const source = readFileSync(
path.resolve(__dirname, "../../src/session/system.ts"),
"utf8",
)
// The environment() function builds the env string via a .join("\n") on an
// array. The first element of that array must be `"\nYou are powered by..."
// (i.e. the literal starts with a newline escape or a backtick-newline).
// We match the template literal opening that starts the env block string.
const match = source.match(/`\\nYou are powered by the model/)
expect(match).not.toBeNull()
})
Loading