|
|
@@ -7,12 +7,9 @@ import { APICallError } from "ai"
|
|
|
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Schema } from "effect"
|
|
|
import * as Stream from "effect/Stream"
|
|
|
import { Config } from "@/config/config"
|
|
|
-import { Image } from "@/image/image"
|
|
|
-import { Agent } from "../../src/agent/agent"
|
|
|
import { LLM } from "../../src/session/llm"
|
|
|
import { SessionCompaction } from "../../src/session/compaction"
|
|
|
import { Token } from "@/util/token"
|
|
|
-import { Permission } from "../../src/permission"
|
|
|
import { Plugin } from "../../src/plugin"
|
|
|
import { provideTmpdirInstance, TestInstance } from "../fixture/fixture"
|
|
|
import { Session as SessionNs } from "@/session/session"
|
|
|
@@ -21,12 +18,11 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
|
|
import { SessionStatus } from "../../src/session/status"
|
|
|
import { SessionSummary } from "../../src/session/summary"
|
|
|
import { SessionV2 } from "@opencode-ai/core/session"
|
|
|
-import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
|
|
|
import { SessionExecution } from "@opencode-ai/core/session/execution"
|
|
|
+import { SessionProjector } from "@opencode-ai/core/session/projector"
|
|
|
|
|
|
-import type { Provider } from "@/provider/provider"
|
|
|
+import { Provider } from "@/provider/provider"
|
|
|
import * as SessionProcessorModule from "../../src/session/processor"
|
|
|
-import { Snapshot } from "../../src/snapshot"
|
|
|
import { ProviderTest } from "../fake/provider"
|
|
|
import { testEffect } from "../lib/effect"
|
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
|
@@ -35,6 +31,8 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
|
|
import { LLMEvent, Usage } from "@opencode-ai/llm"
|
|
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
|
import { ModelV2 } from "@opencode-ai/core/model"
|
|
|
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
|
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
|
|
|
|
const summary = Layer.succeed(
|
|
|
SessionSummary.Service,
|
|
|
@@ -210,7 +208,7 @@ function fake(
|
|
|
} satisfies SessionProcessorModule.SessionProcessor.Handle
|
|
|
}
|
|
|
|
|
|
-function layer(result: "continue" | "compact") {
|
|
|
+function processorLayer(result: "continue" | "compact") {
|
|
|
return Layer.succeed(
|
|
|
SessionProcessorModule.SessionProcessor.Service,
|
|
|
SessionProcessorModule.SessionProcessor.Service.of({
|
|
|
@@ -221,38 +219,28 @@ function layer(result: "continue" | "compact") {
|
|
|
|
|
|
function cfg(compaction?: ConfigV1.Info["compaction"]) {
|
|
|
const base = Schema.decodeUnknownSync(ConfigV1.Info)({}) as ConfigV1.Info
|
|
|
- return TestConfig.layer({
|
|
|
- get: () => Effect.succeed({ ...base, compaction }),
|
|
|
- })
|
|
|
+ return Layer.succeed(Config.Service, TestConfig.make({ get: () => Effect.succeed({ ...base, compaction }) }))
|
|
|
}
|
|
|
|
|
|
-const deps = Layer.mergeAll(
|
|
|
- wide().layer,
|
|
|
- layer("continue"),
|
|
|
- Agent.defaultLayer,
|
|
|
- Plugin.defaultLayer,
|
|
|
- EventV2Bridge.defaultLayer,
|
|
|
- Config.defaultLayer,
|
|
|
- RuntimeFlags.layer({ experimentalEventSystem: true }),
|
|
|
- Database.defaultLayer,
|
|
|
- EventV2Bridge.defaultLayer,
|
|
|
-)
|
|
|
-
|
|
|
-const env = Layer.mergeAll(
|
|
|
- SessionNs.defaultLayer,
|
|
|
- Database.defaultLayer,
|
|
|
- EventV2Bridge.defaultLayer,
|
|
|
- CrossSpawnSpawner.defaultLayer,
|
|
|
- SessionCompaction.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provideMerge(deps)),
|
|
|
-)
|
|
|
+const defaultProvider = wide()
|
|
|
+const compactionTestNode = LayerNode.group([
|
|
|
+ SessionCompaction.node,
|
|
|
+ SessionNs.node,
|
|
|
+ SessionProjector.node,
|
|
|
+ Database.node,
|
|
|
+ EventV2Bridge.node,
|
|
|
+ CrossSpawnSpawner.node,
|
|
|
+])
|
|
|
+const env = AppNodeBuilder.build(compactionTestNode, [
|
|
|
+ [Provider.node, defaultProvider.layer],
|
|
|
+ [SessionProcessorModule.SessionProcessor.node, processorLayer("continue")],
|
|
|
+ [RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
|
|
+])
|
|
|
|
|
|
const it = testEffect(env)
|
|
|
|
|
|
-const compactionEnv = Layer.mergeAll(
|
|
|
- SessionNs.defaultLayer,
|
|
|
- Database.defaultLayer,
|
|
|
- EventV2Bridge.defaultLayer,
|
|
|
- CrossSpawnSpawner.defaultLayer,
|
|
|
+const compactionEnv = AppNodeBuilder.build(
|
|
|
+ LayerNode.group([SessionNs.node, SessionProjector.node, Database.node, EventV2Bridge.node, CrossSpawnSpawner.node]),
|
|
|
)
|
|
|
const itCompaction = testEffect(compactionEnv)
|
|
|
|
|
|
@@ -260,7 +248,7 @@ type CompactionProcessOptions = {
|
|
|
result?: "continue" | "compact"
|
|
|
llm?: Layer.Layer<LLM.Service>
|
|
|
plugin?: Layer.Layer<Plugin.Service>
|
|
|
- provider?: ReturnType<typeof ProviderTest.fake>
|
|
|
+ provider?: ReturnType<typeof wide>
|
|
|
config?: Layer.Layer<Config.Service>
|
|
|
}
|
|
|
|
|
|
@@ -269,30 +257,25 @@ function withCompaction(options?: CompactionProcessOptions) {
|
|
|
}
|
|
|
|
|
|
function compactionProcessLayer(options?: CompactionProcessOptions) {
|
|
|
- const events = EventV2Bridge.defaultLayer
|
|
|
- const status = SessionStatus.layer.pipe(Layer.provide(events))
|
|
|
- const processor = options?.llm
|
|
|
- ? SessionProcessorModule.SessionProcessor.layer.pipe(
|
|
|
- Layer.provide(summary),
|
|
|
- Layer.provide(Image.defaultLayer),
|
|
|
- Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
|
|
- Layer.provide(status),
|
|
|
- )
|
|
|
- : layer(options?.result ?? "continue")
|
|
|
- return Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, events, status).pipe(
|
|
|
- Layer.provide(SessionNs.defaultLayer),
|
|
|
- Layer.provide((options?.provider ?? wide()).layer),
|
|
|
- Layer.provide(Snapshot.defaultLayer),
|
|
|
- Layer.provide(options?.llm ?? LLM.defaultLayer),
|
|
|
- Layer.provide(Permission.defaultLayer),
|
|
|
- Layer.provide(Agent.defaultLayer),
|
|
|
- Layer.provide(options?.plugin ?? Plugin.defaultLayer),
|
|
|
- Layer.provide(status),
|
|
|
- Layer.provide(events),
|
|
|
- Layer.provide(options?.config ?? Config.defaultLayer),
|
|
|
- Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
|
|
- Layer.provide(EventV2Bridge.defaultLayer),
|
|
|
- )
|
|
|
+ const replacements: LayerNode.Replacements = [
|
|
|
+ [Provider.node, (options?.provider ?? wide()).layer],
|
|
|
+ [RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
|
|
|
+ [SessionSummary.node, summary],
|
|
|
+ ]
|
|
|
+ if (!options?.llm) {
|
|
|
+ return AppNodeBuilder.build(compactionTestNode, [
|
|
|
+ ...replacements,
|
|
|
+ [SessionProcessorModule.SessionProcessor.node, processorLayer(options?.result ?? "continue")],
|
|
|
+ ...(options?.plugin ? ([[Plugin.node, options.plugin]] as const) : []),
|
|
|
+ ...(options?.config ? ([[Config.node, options.config]] as const) : []),
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ return AppNodeBuilder.build(compactionTestNode, [
|
|
|
+ ...replacements,
|
|
|
+ [LLM.node, options.llm],
|
|
|
+ ...(options?.plugin ? ([[Plugin.node, options.plugin]] as const) : []),
|
|
|
+ ...(options?.config ? ([[Config.node, options.config]] as const) : []),
|
|
|
+ ])
|
|
|
}
|
|
|
|
|
|
function createSummaryCompaction(sessionID: SessionID) {
|
|
|
@@ -318,7 +301,7 @@ function llm() {
|
|
|
push(stream: Stream.Stream<LLMEvent, unknown> | ((input: LLM.StreamInput) => Stream.Stream<LLMEvent, unknown>)) {
|
|
|
queue.push(stream)
|
|
|
},
|
|
|
- layer: Layer.succeed(
|
|
|
+ llmLayer: Layer.succeed(
|
|
|
LLM.Service,
|
|
|
LLM.Service.of({
|
|
|
stream: (input) => {
|
|
|
@@ -614,9 +597,7 @@ describe("session.compaction.create", () => {
|
|
|
})
|
|
|
|
|
|
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
|
|
- Effect.provide(SessionV2.defaultLayer),
|
|
|
- Effect.provide(SessionExecution.noopLayer),
|
|
|
- Effect.provide(locationServiceMapLayer),
|
|
|
+ Effect.provide(AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]])),
|
|
|
)
|
|
|
expect(v2.at(-1)).toMatchObject({
|
|
|
type: "compaction",
|
|
|
@@ -1014,7 +995,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(part?.type).toBe("compaction")
|
|
|
expect(part?.tail_start_id).toBeUndefined()
|
|
|
expect(captured).toContain("yyyy")
|
|
|
- }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 20 }) }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1051,7 +1032,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(part?.tail_start_id).toBeUndefined()
|
|
|
expect(captured).toContain("recent image turn")
|
|
|
expect(captured).toContain("Attached image/png: big.png")
|
|
|
- }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1102,7 +1083,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(filtered[1]?.info.role).toBe("assistant")
|
|
|
expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true)
|
|
|
expect(filtered.map((msg) => msg.info.id)).not.toContain(large.id)
|
|
|
- }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 100 }) }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1263,7 +1244,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(Cause.hasInterrupts(exit.cause)).toBe(true)
|
|
|
expect(Date.now() - start).toBeLessThan(250)
|
|
|
}
|
|
|
- }).pipe(withCompaction({ llm: stub.layer }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
{ timeout: 10_000 },
|
|
|
@@ -1336,7 +1317,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(summary?.parts.some((part) => part.type === "reasoning")).toBe(false)
|
|
|
// Sanity: the text part still got through.
|
|
|
expect(summary?.parts.some((part) => part.type === "text" && part.text === "summary")).toBe(true)
|
|
|
- }).pipe(withCompaction({ llm: stub.layer }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1372,7 +1353,7 @@ describe("session.compaction.process", () => {
|
|
|
|
|
|
expect(summary?.info.role).toBe("assistant")
|
|
|
expect(summary?.parts.some((part) => part.type === "tool")).toBe(false)
|
|
|
- }).pipe(withCompaction({ llm: stub.layer }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1409,7 +1390,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(captured).not.toContain("keep this turn")
|
|
|
expect(captured).not.toContain("and this one too")
|
|
|
expect(captured).not.toContain("What did we do so far?")
|
|
|
- }).pipe(withCompaction({ llm: stub.layer }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1451,7 +1432,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(captured.match(/summary one/g)?.length).toBe(1)
|
|
|
expect(captured).toContain("## Constraints & Preferences")
|
|
|
expect(captured).toContain("## Progress")
|
|
|
- }).pipe(withCompaction({ llm: stub.layer }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer }))
|
|
|
},
|
|
|
{ git: true },
|
|
|
)
|
|
|
@@ -1493,7 +1474,7 @@ describe("session.compaction.process", () => {
|
|
|
expect(
|
|
|
filtered.some((msg) => msg.info.role === "user" && msg.parts.some((part) => part.type === "compaction")),
|
|
|
).toBe(true)
|
|
|
- }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) }))
|
|
|
+ }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) }))
|
|
|
})
|
|
|
|
|
|
itCompaction.instance(
|