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
4 changes: 4 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { ConfigPermission } from "./permission"
import { ConfigPlugin } from "./plugin"
import { ConfigProvider } from "./provider"
import { ConfigServer } from "./server"
import { ConfigSession } from "./session"
import { ConfigSkills } from "./skills"
import { ConfigVariable } from "./variable"
import { Npm } from "@opencode-ai/core/npm"
Expand Down Expand Up @@ -108,6 +109,9 @@ export const Info = Schema.Struct({
server: Schema.optional(ConfigServer.Server).annotate({
description: "Server configuration for opencode serve and web commands",
}),
session: Schema.optional(ConfigSession.Info).annotate({
description: "Session behavior configuration",
}),
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
description: "Command configuration, see https://opencode.ai/docs/commands",
}),
Expand Down
16 changes: 16 additions & 0 deletions packages/opencode/src/config/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Schema } from "effect"
import { zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"

export const Info = Schema.Struct({
summarize: Schema.optional(Schema.Boolean).annotate({
description:
"Enable automatic session diff summarization during prompt processing. When false, opencode skips background session summary and per-message diff updates. Defaults to true.",
}),
})
.annotate({ identifier: "SessionConfig" })
.pipe(withStatics((s) => ({ zod: zod(s) })))

export type Info = Schema.Schema.Type<typeof Info>

export * as ConfigSession from "./session"
13 changes: 7 additions & 6 deletions packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,13 @@ export const layer: Layer.Layer<
}
ctx.snapshot = undefined
}
yield* summary
.summarize({
sessionID: ctx.sessionID,
messageID: ctx.assistantMessage.parentID,
})
.pipe(Effect.ignore, Effect.forkIn(scope))
if ((yield* config.get()).session?.summarize !== false)
yield* summary
.summarize({
sessionID: ctx.sessionID,
messageID: ctx.assistantMessage.parentID,
})
.pipe(Effect.ignore, Effect.forkIn(scope))
if (
!ctx.assistantMessage.summary &&
isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})
}

if (step === 1)
if (step === 1 && (yield* config.get()).session?.summarize !== false)
yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope))

if (step > 1 && lastFinished) {
Expand Down
20 changes: 20 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@ test("loads JSON config file", async () => {
})
})

test("loads session summarize config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await writeConfig(dir, {
$schema: "https://opencode.ai/config.json",
session: {
summarize: false,
},
})
},
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const config = await load()
expect(config.session?.summarize).toBe(false)
},
})
})

test("loads shell config field", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
Expand Down
66 changes: 65 additions & 1 deletion packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ import { reply, TestLLMServer } from "../lib/llm-server"

void Log.init({ print: false })

let summarizeCalls = 0

const summary = Layer.succeed(
SessionSummary.Service,
SessionSummary.Service.of({
summarize: () => Effect.void,
summarize: () =>
Effect.sync(() => {
summarizeCalls++
}),
diff: () => Effect.succeed([]),
computeDiff: () => Effect.succeed([]),
}),
Expand Down Expand Up @@ -254,6 +259,13 @@ function providerCfg(url: string) {
}
}

function providerCfgWithSession(url: string, session: Config.Info["session"]) {
return {
...providerCfg(url),
session,
}
}

const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: string) {
const session = yield* Session.Service
const msg = yield* session.updateMessage({
Expand Down Expand Up @@ -415,6 +427,58 @@ it.live("prompt emits v2 prompted and synthetic events", () =>
),
)

it.live("loop schedules automatic summaries by default", () =>
provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) {
summarizeCalls = 0
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({
title: "Pinned",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
})
yield* prompt.prompt({
sessionID: chat.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
yield* llm.text("world")

yield* prompt.loop({ sessionID: chat.id })
yield* Effect.sleep("10 millis")
expect(summarizeCalls).toBe(2)
}),
{ git: true, config: providerCfg },
),
)

it.live("loop skips session summary when session.summarize is false", () =>
provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) {
summarizeCalls = 0
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({
title: "Pinned",
permission: [{ permission: "*", pattern: "*", action: "allow" }],
})
yield* prompt.prompt({
sessionID: chat.id,
agent: "build",
noReply: true,
parts: [{ type: "text", text: "hello" }],
})
yield* llm.text("world")

yield* prompt.loop({ sessionID: chat.id })
yield* Effect.sleep("10 millis")
expect(summarizeCalls).toBe(0)
}),
{ git: true, config: (url) => providerCfgWithSession(url, { summarize: false }) },
),
)

it.live("static loop returns assistant text through local provider", () =>
provideTmpdirServer(
Effect.fnUntraced(function* ({ llm }) {
Expand Down
Loading