|
@@ -34,6 +34,8 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
|
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
|
|
|
|
+import { Config } from "@opencode-ai/core/config"
|
|
|
|
|
+import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
|
|
|
import { NativeTool } from "@opencode-ai/core/tool/native"
|
|
import { NativeTool } from "@opencode-ai/core/tool/native"
|
|
|
import {
|
|
import {
|
|
|
SessionContextEpochTable,
|
|
SessionContextEpochTable,
|
|
@@ -96,6 +98,11 @@ const client = Layer.succeed(
|
|
|
)
|
|
)
|
|
|
const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
|
|
const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
|
|
|
const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
|
|
const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
|
|
|
|
|
+const compactModel = Model.make({
|
|
|
|
|
+ id: "compact",
|
|
|
|
|
+ provider: "fake",
|
|
|
|
|
+ route: OpenAIChat.route.with({ limits: { context: 4_000, output: 50 } }),
|
|
|
|
|
+})
|
|
|
const authorizations: ToolRegistry.AuthorizeInput[] = []
|
|
const authorizations: ToolRegistry.AuthorizeInput[] = []
|
|
|
const executions: string[] = []
|
|
const executions: string[] = []
|
|
|
const permission = Layer.succeed(
|
|
const permission = Layer.succeed(
|
|
@@ -150,8 +157,9 @@ const echo = Layer.effectDiscard(
|
|
|
),
|
|
),
|
|
|
).pipe(Layer.provide(registry))
|
|
).pipe(Layer.provide(registry))
|
|
|
let modelResolveHook = Effect.void
|
|
let modelResolveHook = Effect.void
|
|
|
|
|
+let currentModel = model
|
|
|
const models = SessionRunnerModel.layerWith((session) =>
|
|
const models = SessionRunnerModel.layerWith((session) =>
|
|
|
- modelResolveHook.pipe(Effect.as(session.model?.id === "replacement" ? replacementModel : model)),
|
|
|
|
|
|
|
+ modelResolveHook.pipe(Effect.as(session.model?.id === "replacement" ? replacementModel : currentModel)),
|
|
|
)
|
|
)
|
|
|
const systemContextKey = SystemContext.Key.make("test/context")
|
|
const systemContextKey = SystemContext.Key.make("test/context")
|
|
|
let systemBaseline = "Initial context"
|
|
let systemBaseline = "Initial context"
|
|
@@ -204,6 +212,23 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
|
|
: SystemContext.empty,
|
|
: SystemContext.empty,
|
|
|
),
|
|
),
|
|
|
})
|
|
})
|
|
|
|
|
+const config = Layer.succeed(
|
|
|
|
|
+ Config.Service,
|
|
|
|
|
+ Config.Service.of({
|
|
|
|
|
+ entries: () =>
|
|
|
|
|
+ Effect.succeed([
|
|
|
|
|
+ new Config.Document({
|
|
|
|
|
+ type: "document",
|
|
|
|
|
+ info: new Config.Info({
|
|
|
|
|
+ compaction: new ConfigCompaction.Info({
|
|
|
|
|
+ buffer: 3_000,
|
|
|
|
|
+ keep: new ConfigCompaction.Keep({ tokens: 1_000 }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ ]),
|
|
|
|
|
+ }),
|
|
|
|
|
+)
|
|
|
const runner = SessionRunnerLLM.layer.pipe(
|
|
const runner = SessionRunnerLLM.layer.pipe(
|
|
|
Layer.provide(database),
|
|
Layer.provide(database),
|
|
|
Layer.provide(store),
|
|
Layer.provide(store),
|
|
@@ -215,6 +240,7 @@ const runner = SessionRunnerLLM.layer.pipe(
|
|
|
Layer.provide(location),
|
|
Layer.provide(location),
|
|
|
Layer.provide(agents),
|
|
Layer.provide(agents),
|
|
|
Layer.provide(skillGuidance),
|
|
Layer.provide(skillGuidance),
|
|
|
|
|
+ Layer.provide(config),
|
|
|
)
|
|
)
|
|
|
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
|
const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner))
|
|
|
const execution = Layer.effect(
|
|
const execution = Layer.effect(
|
|
@@ -253,6 +279,7 @@ const it = testEffect(
|
|
|
systemContext,
|
|
systemContext,
|
|
|
location,
|
|
location,
|
|
|
skillGuidance,
|
|
skillGuidance,
|
|
|
|
|
+ config,
|
|
|
runner,
|
|
runner,
|
|
|
coordinator,
|
|
coordinator,
|
|
|
execution,
|
|
execution,
|
|
@@ -288,6 +315,7 @@ const setup = Effect.gen(function* () {
|
|
|
systemUnavailable = false
|
|
systemUnavailable = false
|
|
|
systemLoadHook = Effect.void
|
|
systemLoadHook = Effect.void
|
|
|
modelResolveHook = Effect.void
|
|
modelResolveHook = Effect.void
|
|
|
|
|
+ currentModel = model
|
|
|
skillBaselines.clear()
|
|
skillBaselines.clear()
|
|
|
responses = undefined
|
|
responses = undefined
|
|
|
streamFailure = undefined
|
|
streamFailure = undefined
|
|
@@ -1337,16 +1365,20 @@ describe("SessionRunnerLLM", () => {
|
|
|
requests.length = 0
|
|
requests.length = 0
|
|
|
response = []
|
|
response = []
|
|
|
yield* session.resume(sessionID)
|
|
yield* session.resume(sessionID)
|
|
|
|
|
+ const compactionID = SessionMessage.ID.create()
|
|
|
yield* events.publish(SessionEvent.Compaction.Started, {
|
|
yield* events.publish(SessionEvent.Compaction.Started, {
|
|
|
sessionID,
|
|
sessionID,
|
|
|
- messageID: SessionMessage.ID.create(),
|
|
|
|
|
|
|
+ messageID: compactionID,
|
|
|
timestamp: DateTime.makeUnsafe(1),
|
|
timestamp: DateTime.makeUnsafe(1),
|
|
|
reason: "manual",
|
|
reason: "manual",
|
|
|
})
|
|
})
|
|
|
yield* events.publish(SessionEvent.Compaction.Ended, {
|
|
yield* events.publish(SessionEvent.Compaction.Ended, {
|
|
|
sessionID,
|
|
sessionID,
|
|
|
|
|
+ messageID: compactionID,
|
|
|
timestamp: DateTime.makeUnsafe(2),
|
|
timestamp: DateTime.makeUnsafe(2),
|
|
|
|
|
+ reason: "manual",
|
|
|
text: "summary",
|
|
text: "summary",
|
|
|
|
|
+ recent: "",
|
|
|
})
|
|
})
|
|
|
systemBaseline = "Replacement context"
|
|
systemBaseline = "Replacement context"
|
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
|
|
@@ -1362,6 +1394,68 @@ describe("SessionRunnerLLM", () => {
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ it.effect("automatically compacts into a completed summary and retained recent turn", () =>
|
|
|
|
|
+ Effect.gen(function* () {
|
|
|
|
|
+ yield* setup
|
|
|
|
|
+ const session = yield* SessionV2.Service
|
|
|
|
|
+ response = fragmentFixture("text", "text-first", ["Earlier answer"]).completeEvents
|
|
|
|
|
+ yield* session.prompt({
|
|
|
|
|
+ sessionID,
|
|
|
|
|
+ prompt: new Prompt({ text: "Earlier question ".repeat(180) }),
|
|
|
|
|
+ resume: false,
|
|
|
|
|
+ })
|
|
|
|
|
+ yield* session.resume(sessionID)
|
|
|
|
|
+
|
|
|
|
|
+ currentModel = compactModel
|
|
|
|
|
+ requests.length = 0
|
|
|
|
|
+ responses = [
|
|
|
|
|
+ fragmentFixture("text", "text-summary", ["## Goal\n- Preserve the task"]).completeEvents,
|
|
|
|
|
+ fragmentFixture("text", "text-final", ["Continued"]).completeEvents,
|
|
|
|
|
+ ]
|
|
|
|
|
+ yield* session.prompt({
|
|
|
|
|
+ sessionID,
|
|
|
|
|
+ prompt: new Prompt({ text: "Recent exact request ".repeat(180) }),
|
|
|
|
|
+ resume: false,
|
|
|
|
|
+ })
|
|
|
|
|
+ yield* session.resume(sessionID)
|
|
|
|
|
+
|
|
|
|
|
+ expect(requests).toHaveLength(2)
|
|
|
|
|
+ expect(userTexts(requests[0])[0]).toContain("## Goal")
|
|
|
|
|
+ expect(userTexts(requests[1])).toHaveLength(1)
|
|
|
|
|
+ expect(userTexts(requests[1])[0]).toContain("<summary>\n## Goal\n- Preserve the task\n</summary>")
|
|
|
|
|
+ expect(userTexts(requests[1])[0]).toContain(`[User]: ${"Recent exact request ".repeat(180)}`)
|
|
|
|
|
+
|
|
|
|
|
+ const context = yield* (yield* SessionStore.Service).context(sessionID)
|
|
|
|
|
+ expect(context.map((message) => message.type)).toEqual(["compaction", "assistant"])
|
|
|
|
|
+ expect(context[0]).toMatchObject({
|
|
|
|
|
+ type: "compaction",
|
|
|
|
|
+ summary: "## Goal\n- Preserve the task",
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ requests.length = 0
|
|
|
|
|
+ responses = [
|
|
|
|
|
+ fragmentFixture("text", "text-summary-2", ["## Goal\n- Preserve the updated task"]).completeEvents,
|
|
|
|
|
+ fragmentFixture("text", "text-final-2", ["Continued again"]).completeEvents,
|
|
|
|
|
+ ]
|
|
|
|
|
+ yield* session.prompt({
|
|
|
|
|
+ sessionID,
|
|
|
|
|
+ prompt: new Prompt({ text: "Newest exact request ".repeat(180) }),
|
|
|
|
|
+ resume: false,
|
|
|
|
|
+ })
|
|
|
|
|
+ yield* session.resume(sessionID)
|
|
|
|
|
+
|
|
|
|
|
+ expect(requests).toHaveLength(2)
|
|
|
|
|
+ expect(userTexts(requests[0])[0]).toContain(
|
|
|
|
|
+ "<previous-summary>\n## Goal\n- Preserve the task\n</previous-summary>",
|
|
|
|
|
+ )
|
|
|
|
|
+ expect(userTexts(requests[0])[0]).toContain("Recent exact request")
|
|
|
|
|
+ expect((yield* (yield* SessionStore.Service).context(sessionID))[0]).toMatchObject({
|
|
|
|
|
+ type: "compaction",
|
|
|
|
|
+ summary: "## Goal\n- Preserve the updated task",
|
|
|
|
|
+ })
|
|
|
|
|
+ }),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
it.effect("preserves effective System updates while compaction replacement is blocked", () =>
|
|
it.effect("preserves effective System updates while compaction replacement is blocked", () =>
|
|
|
Effect.gen(function* () {
|
|
Effect.gen(function* () {
|
|
|
yield* setup
|
|
yield* setup
|
|
@@ -1375,16 +1469,20 @@ describe("SessionRunnerLLM", () => {
|
|
|
systemBaseline = "Changed context"
|
|
systemBaseline = "Changed context"
|
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false })
|
|
|
yield* session.resume(sessionID)
|
|
yield* session.resume(sessionID)
|
|
|
|
|
+ const compactionID = SessionMessage.ID.create()
|
|
|
yield* events.publish(SessionEvent.Compaction.Started, {
|
|
yield* events.publish(SessionEvent.Compaction.Started, {
|
|
|
sessionID,
|
|
sessionID,
|
|
|
- messageID: SessionMessage.ID.create(),
|
|
|
|
|
|
|
+ messageID: compactionID,
|
|
|
timestamp: DateTime.makeUnsafe(1),
|
|
timestamp: DateTime.makeUnsafe(1),
|
|
|
reason: "manual",
|
|
reason: "manual",
|
|
|
})
|
|
})
|
|
|
yield* events.publish(SessionEvent.Compaction.Ended, {
|
|
yield* events.publish(SessionEvent.Compaction.Ended, {
|
|
|
sessionID,
|
|
sessionID,
|
|
|
|
|
+ messageID: compactionID,
|
|
|
timestamp: DateTime.makeUnsafe(2),
|
|
timestamp: DateTime.makeUnsafe(2),
|
|
|
|
|
+ reason: "manual",
|
|
|
text: "summary",
|
|
text: "summary",
|
|
|
|
|
+ recent: "",
|
|
|
})
|
|
})
|
|
|
systemUnavailable = true
|
|
systemUnavailable = true
|
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
|
|
yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false })
|