refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { APICallError } from "ai"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { Image } from "@/image/image"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
@@ -18,8 +20,8 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { SessionV2 } from "../../src/v2/session"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import * as SessionProcessorModule from "../../src/session/processor"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
@@ -27,10 +29,9 @@ import { ProviderTest } from "../fake/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { TestConfig } from "../fixture/config"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { LLMEvent, Usage } from "@opencode-ai/llm"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -44,8 +45,8 @@ const summary = Layer.succeed(
|
||||
)
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ModelID.make("test-model"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
}
|
||||
|
||||
const usage = (input: ConstructorParameters<typeof Usage>[0]) => new Usage(input)
|
||||
@@ -229,22 +230,24 @@ const deps = Layer.mergeAll(
|
||||
layer("continue"),
|
||||
Agent.defaultLayer,
|
||||
Plugin.defaultLayer,
|
||||
Bus.layer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
Config.defaultLayer,
|
||||
SyncEvent.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 it = testEffect(env)
|
||||
|
||||
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
||||
const compactionEnv = Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer, EventV2Bridge.defaultLayer, CrossSpawnSpawner.defaultLayer)
|
||||
const itCompaction = testEffect(compactionEnv)
|
||||
|
||||
type CompactionProcessOptions = {
|
||||
@@ -260,8 +263,8 @@ function withCompaction(options?: CompactionProcessOptions) {
|
||||
}
|
||||
|
||||
function compactionProcessLayer(options?: CompactionProcessOptions) {
|
||||
const bus = Bus.layer
|
||||
const status = SessionStatus.layer.pipe(Layer.provide(bus))
|
||||
const events = EventV2Bridge.defaultLayer
|
||||
const status = SessionStatus.layer.pipe(Layer.provide(events))
|
||||
const processor = options?.llm
|
||||
? SessionProcessorModule.SessionProcessor.layer.pipe(
|
||||
Layer.provide(summary),
|
||||
@@ -270,7 +273,7 @@ function compactionProcessLayer(options?: CompactionProcessOptions) {
|
||||
Layer.provide(status),
|
||||
)
|
||||
: layer(options?.result ?? "continue")
|
||||
return Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
|
||||
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),
|
||||
@@ -279,9 +282,8 @@ function compactionProcessLayer(options?: CompactionProcessOptions) {
|
||||
Layer.provide(Agent.defaultLayer),
|
||||
Layer.provide(options?.plugin ?? Plugin.defaultLayer),
|
||||
Layer.provide(status),
|
||||
Layer.provide(bus),
|
||||
Layer.provide(events),
|
||||
Layer.provide(options?.config ?? Config.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
)
|
||||
@@ -296,7 +298,7 @@ function readCompactionPart(sessionID: SessionID) {
|
||||
.messages({ sessionID })
|
||||
.pipe(
|
||||
Effect.map((messages) =>
|
||||
messages.at(-2)?.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction"),
|
||||
messages.at(-2)?.parts.find((item): item is SessionLegacy.CompactionPart => item.type === "compaction"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -586,6 +588,26 @@ describe("session.compaction.create", () => {
|
||||
overflow: true,
|
||||
})
|
||||
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live.skip(
|
||||
"projects a compaction message to v2 (v2 projector disabled)",
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
const compact = yield* SessionCompaction.Service
|
||||
const ssn = yield* SessionNs.Service
|
||||
const info = yield* ssn.create({})
|
||||
|
||||
yield* compact.create({
|
||||
sessionID: info.id,
|
||||
agent: "build",
|
||||
model: ref,
|
||||
auto: true,
|
||||
overflow: true,
|
||||
})
|
||||
|
||||
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
||||
Effect.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
@@ -623,7 +645,7 @@ describe("session.compaction.prune", () => {
|
||||
type: "text",
|
||||
text: "first",
|
||||
})
|
||||
const b: MessageV2.Assistant = {
|
||||
const b: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID: info.id,
|
||||
@@ -719,7 +741,7 @@ describe("session.compaction.prune", () => {
|
||||
type: "text",
|
||||
text: "first",
|
||||
})
|
||||
const b: MessageV2.Assistant = {
|
||||
const b: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID: info.id,
|
||||
@@ -821,19 +843,21 @@ describe("session.compaction.process", () => {
|
||||
it.instance(
|
||||
"publishes compacted event on continue",
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const ssn = yield* SessionNs.Service
|
||||
const session = yield* ssn.create({})
|
||||
const msg = yield* createUserMessage(session.id, "hello")
|
||||
const msgs = yield* ssn.messages({ sessionID: session.id })
|
||||
const done = yield* Deferred.make<void, Error>()
|
||||
let seen = false
|
||||
const unsub = yield* bus.subscribeCallback(SessionCompaction.Event.Compacted, (evt) => {
|
||||
if (evt.properties.sessionID !== session.id) return
|
||||
const unsub = yield* events.listen((evt) => {
|
||||
if (evt.type !== SessionCompaction.Event.Compacted.type) return Effect.void
|
||||
if ((evt.data as typeof SessionCompaction.Event.Compacted.data.Type).sessionID !== session.id) return Effect.void
|
||||
seen = true
|
||||
Deferred.doneUnsafe(done, Effect.void)
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsub))
|
||||
yield* Effect.addFinalizer(() => unsub)
|
||||
|
||||
const result = yield* SessionCompaction.use.process({
|
||||
parentID: msg.id,
|
||||
@@ -1064,7 +1088,7 @@ describe("session.compaction.process", () => {
|
||||
expect(captured).toContain("zzzz")
|
||||
expect(captured).not.toContain("keep tail")
|
||||
|
||||
const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
const filtered = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
expect(filtered.map((msg) => msg.info.id).slice(0, 3)).toEqual([parent!, expect.any(String), keep.id])
|
||||
expect(filtered[1]?.info.role).toBe("assistant")
|
||||
expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true)
|
||||
@@ -1197,17 +1221,19 @@ describe("session.compaction.process", () => {
|
||||
|
||||
return Effect.gen(function* () {
|
||||
const ssn = yield* SessionNs.Service
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const ready = yield* Deferred.make<void>()
|
||||
const session = yield* ssn.create({})
|
||||
const msg = yield* createUserMessage(session.id, "hello")
|
||||
const msgs = yield* ssn.messages({ sessionID: session.id })
|
||||
const off = yield* bus.subscribeCallback(SessionStatus.Event.Status, (evt) => {
|
||||
if (evt.properties.sessionID !== session.id) return
|
||||
if (evt.properties.status.type !== "retry") return
|
||||
const off = yield* events.listen((evt) => {
|
||||
if (evt.type !== SessionStatus.Event.Status.type) return Effect.void
|
||||
const data = evt.data as typeof SessionStatus.Event.Status.data.Type
|
||||
if (data.sessionID !== session.id || data.status.type !== "retry") return Effect.void
|
||||
Deferred.doneUnsafe(ready, Effect.void)
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(off))
|
||||
yield* Effect.addFinalizer(() => off)
|
||||
|
||||
const fiber = yield* SessionCompaction.use
|
||||
.process({
|
||||
@@ -1405,7 +1431,7 @@ describe("session.compaction.process", () => {
|
||||
yield* createUserMessage(session.id, "latest turn")
|
||||
yield* createCompactionMarker(session.id)
|
||||
|
||||
msgs = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||
@@ -1441,12 +1467,12 @@ describe("session.compaction.process", () => {
|
||||
const u4 = yield* createUserMessage(session.id, "four")
|
||||
yield* createCompactionMarker(session.id)
|
||||
|
||||
msgs = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
parent = msgs.at(-1)?.info.id
|
||||
expect(parent).toBeTruthy()
|
||||
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
|
||||
|
||||
const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
const filtered = MessageV2.filterCompacted(yield* MessageV2.stream(session.id))
|
||||
const ids = filtered.map((msg) => msg.info.id)
|
||||
|
||||
expect(ids).not.toContain(u1.id)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import path from "path"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { Instruction } from "../../src/session/instruction"
|
||||
import type { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { RuntimeFlags } from "../../src/effect/runtime-flags"
|
||||
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstance, provideTmpdirInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { TestConfig } from "../fixture/config"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
|
||||
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer))
|
||||
|
||||
const configLayer = TestConfig.layer()
|
||||
|
||||
@@ -61,7 +63,7 @@ const tmpWithFiles = (files: Record<string, string>) =>
|
||||
return dir
|
||||
})
|
||||
|
||||
function loaded(filepath: string): MessageV2.WithParts[] {
|
||||
function loaded(filepath: string): SessionLegacy.WithParts[] {
|
||||
const sessionID = SessionID.make("session-loaded-1")
|
||||
const messageID = MessageID.make("msg_message-loaded-1")
|
||||
|
||||
@@ -74,8 +76,8 @@ function loaded(filepath: string): MessageV2.WithParts[] {
|
||||
time: { created: 0 },
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: ProviderID.make("anthropic"),
|
||||
modelID: ModelID.make("claude-sonnet-4-20250514"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
modelID: ProviderV2.ModelID.make("claude-sonnet-4-20250514"),
|
||||
},
|
||||
},
|
||||
parts: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
@@ -13,7 +14,7 @@ import { Auth } from "@/auth"
|
||||
import { Config } from "@/config/config"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { LLMEvent, LLMResponse } from "@opencode-ai/llm"
|
||||
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
|
||||
@@ -25,6 +26,7 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "../fixtures/recordings")
|
||||
|
||||
@@ -41,7 +43,7 @@ const replayOpenAIOAuth = {
|
||||
type RecordedScenario = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly providerID: ProviderID
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly modelID: string
|
||||
readonly cassette: string
|
||||
readonly protocol: string
|
||||
@@ -88,7 +90,7 @@ function decodeRecordOpenAIOAuth() {
|
||||
}
|
||||
|
||||
const providerConfig = (input: {
|
||||
readonly providerID: ProviderID
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly name: string
|
||||
readonly env: string[]
|
||||
readonly npm: string
|
||||
@@ -113,7 +115,7 @@ const RECORDED_SCENARIOS = [
|
||||
{
|
||||
id: "openai-api-key",
|
||||
name: "OpenAI API key",
|
||||
providerID: ProviderID.openai,
|
||||
providerID: ProviderV2.ID.openai,
|
||||
modelID: "gpt-4.1-mini",
|
||||
cassette: "session/native-openai-tool-loop",
|
||||
protocol: "openai-responses",
|
||||
@@ -121,7 +123,7 @@ const RECORDED_SCENARIOS = [
|
||||
canRecord: () => Boolean(envValue("OPENCODE_RECORD_OPENAI_API_KEY", "OPENAI_API_KEY")),
|
||||
config: (model) =>
|
||||
providerConfig({
|
||||
providerID: ProviderID.openai,
|
||||
providerID: ProviderV2.ID.openai,
|
||||
name: "OpenAI",
|
||||
env: ["OPENAI_API_KEY"],
|
||||
npm: "@ai-sdk/openai",
|
||||
@@ -136,7 +138,7 @@ const RECORDED_SCENARIOS = [
|
||||
{
|
||||
id: "openai-oauth",
|
||||
name: "OpenAI OAuth",
|
||||
providerID: ProviderID.openai,
|
||||
providerID: ProviderV2.ID.openai,
|
||||
modelID: "gpt-5.5",
|
||||
cassette: "session/native-openai-oauth-tool-loop",
|
||||
protocol: "openai-responses",
|
||||
@@ -147,7 +149,7 @@ const RECORDED_SCENARIOS = [
|
||||
stableID: "openai-oauth",
|
||||
config: (model) =>
|
||||
providerConfig({
|
||||
providerID: ProviderID.openai,
|
||||
providerID: ProviderV2.ID.openai,
|
||||
name: "OpenAI",
|
||||
env: ["OPENAI_API_KEY"],
|
||||
npm: "@ai-sdk/openai",
|
||||
@@ -159,7 +161,7 @@ const RECORDED_SCENARIOS = [
|
||||
{
|
||||
id: "opencode-proxy",
|
||||
name: "OpenCode proxy",
|
||||
providerID: ProviderID.opencode,
|
||||
providerID: ProviderV2.ID.opencode,
|
||||
modelID: "gpt-5.2-codex",
|
||||
cassette: "session/native-zen-tool-loop",
|
||||
protocol: "openai-responses",
|
||||
@@ -167,7 +169,7 @@ const RECORDED_SCENARIOS = [
|
||||
canRecord: () => Boolean(process.env.OPENCODE_RECORD_CONSOLE_TOKEN && process.env.OPENCODE_RECORD_ZEN_ORG_ID),
|
||||
config: (model) =>
|
||||
providerConfig({
|
||||
providerID: ProviderID.opencode,
|
||||
providerID: ProviderV2.ID.opencode,
|
||||
name: "OpenCode Zen",
|
||||
env: ["OPENCODE_CONSOLE_TOKEN"],
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
@@ -182,7 +184,7 @@ const RECORDED_SCENARIOS = [
|
||||
{
|
||||
id: "anthropic-api-key",
|
||||
name: "Anthropic API key",
|
||||
providerID: ProviderID.anthropic,
|
||||
providerID: ProviderV2.ID.anthropic,
|
||||
modelID: "claude-haiku-4-5-20251001",
|
||||
cassette: "session/native-anthropic-tool-loop",
|
||||
protocol: "anthropic-messages",
|
||||
@@ -190,7 +192,7 @@ const RECORDED_SCENARIOS = [
|
||||
canRecord: () => Boolean(envValue("OPENCODE_RECORD_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY")),
|
||||
config: (model) =>
|
||||
providerConfig({
|
||||
providerID: ProviderID.anthropic,
|
||||
providerID: ProviderV2.ID.anthropic,
|
||||
name: "Anthropic",
|
||||
env: ["ANTHROPIC_API_KEY"],
|
||||
npm: "@ai-sdk/anthropic",
|
||||
@@ -373,7 +375,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
|
||||
|
||||
const stableID = scenario.stableID ?? scenario.providerID
|
||||
const sessionID = SessionID.make(`session-recorded-${stableID}-loop`)
|
||||
const modelID = ModelID.make(model.id)
|
||||
const modelID = ProviderV2.ModelID.make(model.id)
|
||||
const agent = {
|
||||
name: "test",
|
||||
mode: "primary",
|
||||
@@ -394,7 +396,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
|
||||
time: { created: 0 },
|
||||
agent: agent.name,
|
||||
model: { providerID: scenario.providerID, modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
|
||||
@@ -6,13 +6,14 @@ import { Effect, Layer, Stream } from "effect"
|
||||
import { LLMNative } from "@/session/llm/native-request"
|
||||
import { LLMNativeRuntime } from "@/session/llm/native-runtime"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import { OAUTH_DUMMY_KEY } from "@/auth"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const baseModel: Provider.Model = {
|
||||
id: ModelID.make("gpt-5-mini"),
|
||||
providerID: ProviderID.make("openai"),
|
||||
id: ProviderV2.ModelID.make("gpt-5-mini"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
api: {
|
||||
id: "gpt-5-mini",
|
||||
url: "https://api.openai.com/v1",
|
||||
@@ -62,7 +63,7 @@ const baseModel: Provider.Model = {
|
||||
}
|
||||
|
||||
const providerInfo: Provider.Info = {
|
||||
id: ProviderID.make("openai"),
|
||||
id: ProviderV2.ID.make("openai"),
|
||||
name: "OpenAI",
|
||||
source: "config",
|
||||
env: ["OPENAI_API_KEY"],
|
||||
@@ -354,7 +355,7 @@ describe("session.llm-native.request", () => {
|
||||
const compatible = LLMNative.model({
|
||||
model: {
|
||||
...baseModel,
|
||||
providerID: ProviderID.make("opencode"),
|
||||
providerID: ProviderV2.ID.make("opencode"),
|
||||
api: { ...baseModel.api, url: "https://ai.example.test/v1", npm: "@ai-sdk/openai-compatible" },
|
||||
},
|
||||
apiKey: "test-key",
|
||||
@@ -388,8 +389,8 @@ describe("session.llm-native.request", () => {
|
||||
})
|
||||
expect(
|
||||
LLMNativeRuntime.status({
|
||||
model: { ...baseModel, providerID: ProviderID.make("opencode") },
|
||||
provider: { ...providerInfo, id: ProviderID.make("opencode") },
|
||||
model: { ...baseModel, providerID: ProviderV2.ID.make("opencode") },
|
||||
provider: { ...providerInfo, id: ProviderV2.ID.make("opencode") },
|
||||
auth: undefined,
|
||||
}),
|
||||
).toMatchObject({
|
||||
@@ -400,10 +401,10 @@ describe("session.llm-native.request", () => {
|
||||
LLMNativeRuntime.status({
|
||||
model: {
|
||||
...baseModel,
|
||||
providerID: ProviderID.make("opencode"),
|
||||
providerID: ProviderV2.ID.make("opencode"),
|
||||
api: { ...baseModel.api, npm: "@ai-sdk/openai-compatible" },
|
||||
},
|
||||
provider: { ...providerInfo, id: ProviderID.make("opencode") },
|
||||
provider: { ...providerInfo, id: ProviderV2.ID.make("opencode") },
|
||||
auth: undefined,
|
||||
}),
|
||||
).toMatchObject({
|
||||
@@ -412,8 +413,8 @@ describe("session.llm-native.request", () => {
|
||||
})
|
||||
expect(
|
||||
LLMNativeRuntime.status({
|
||||
model: { ...baseModel, providerID: ProviderID.make("google") },
|
||||
provider: { ...providerInfo, id: ProviderID.make("google") },
|
||||
model: { ...baseModel, providerID: ProviderV2.ID.make("google") },
|
||||
provider: { ...providerInfo, id: ProviderV2.ID.make("google") },
|
||||
auth: undefined,
|
||||
}),
|
||||
).toEqual({ type: "unsupported", reason: "provider is not openai, opencode, or anthropic" })
|
||||
@@ -454,12 +455,12 @@ describe("session.llm-native.request", () => {
|
||||
LLMNativeRuntime.status({
|
||||
model: {
|
||||
...baseModel,
|
||||
providerID: ProviderID.make("anthropic"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
api: { ...baseModel.api, npm: "@ai-sdk/anthropic", url: "https://api.anthropic.com/v1" },
|
||||
},
|
||||
provider: {
|
||||
...providerInfo,
|
||||
id: ProviderID.make("anthropic"),
|
||||
id: ProviderV2.ID.make("anthropic"),
|
||||
name: "Anthropic",
|
||||
env: ["ANTHROPIC_API_KEY"],
|
||||
options: { apiKey: "test-anthropic-key" },
|
||||
@@ -472,10 +473,10 @@ describe("session.llm-native.request", () => {
|
||||
test("prefers console provider api key over stored opencode auth", () => {
|
||||
expect(
|
||||
LLMNativeRuntime.status({
|
||||
model: { ...baseModel, providerID: ProviderID.make("opencode") },
|
||||
model: { ...baseModel, providerID: ProviderV2.ID.make("opencode") },
|
||||
provider: {
|
||||
...providerInfo,
|
||||
id: ProviderID.make("opencode"),
|
||||
id: ProviderV2.ID.make("opencode"),
|
||||
options: { apiKey: "console-token" },
|
||||
key: "zen-token",
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import path from "path"
|
||||
import { tool, type ModelMessage } from "ai"
|
||||
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
@@ -13,7 +14,7 @@ import { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { Plugin } from "@/plugin"
|
||||
import { ProviderID, ModelID } from "../../src/provider/schema"
|
||||
|
||||
import { testEffect } from "../lib/effect"
|
||||
import type { Agent } from "../../src/agent/agent"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -22,6 +23,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Permission } from "@/permission"
|
||||
import { LLMAISDK } from "@/session/llm/ai-sdk"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
type ConfigModel = NonNullable<NonNullable<Config.Info["provider"]>[string]["models"]>[string]
|
||||
|
||||
@@ -712,8 +714,8 @@ describe("session.llm.stream", () => {
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderID.make(vivgridFixture.providerID),
|
||||
ModelID.make(fixture.model.id),
|
||||
ProviderV2.ID.make(vivgridFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-1")
|
||||
const agent = {
|
||||
@@ -731,8 +733,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -786,8 +788,8 @@ describe("session.llm.stream", () => {
|
||||
const pending = waitStreamingRequest("/chat/completions")
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderID.make(alibabaQwenFixture.providerID),
|
||||
ModelID.make(fixture.model.id),
|
||||
ProviderV2.ID.make(alibabaQwenFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-service-abort")
|
||||
const agent = {
|
||||
@@ -802,8 +804,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
const fiber = yield* drain({
|
||||
user,
|
||||
@@ -854,8 +856,8 @@ describe("session.llm.stream", () => {
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderID.make(alibabaQwenFixture.providerID),
|
||||
ModelID.make(fixture.model.id),
|
||||
ProviderV2.ID.make(alibabaQwenFixture.providerID),
|
||||
ProviderV2.ModelID.make(fixture.model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-tools")
|
||||
const agent = {
|
||||
@@ -871,9 +873,9 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
tools: { question: true },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -958,7 +960,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest("/responses", createEventResponse(responseChunks, true))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-2")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -974,8 +976,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1063,7 +1065,7 @@ describe("session.llm.stream", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-flag-off")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1088,8 +1090,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User,
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1133,7 +1135,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest("/responses", createEventResponse(chunks, true))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1150,8 +1152,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User,
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1217,7 +1219,7 @@ describe("session.llm.stream", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-injected-tool")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1233,8 +1235,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User,
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1305,7 +1307,7 @@ describe("session.llm.stream", () => {
|
||||
const request = waitRequest("/responses", createEventResponse(chunks, true))
|
||||
let executed: unknown
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-native-tool")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1321,8 +1323,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User,
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1431,7 +1433,7 @@ describe("session.llm.stream", () => {
|
||||
),
|
||||
).toString("base64")}`
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.openai, ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.openai, ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-data-url")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1446,8 +1448,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1519,8 +1521,8 @@ describe("session.llm.stream", () => {
|
||||
const request = waitRequest("/messages", createEventResponse(chunks))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(
|
||||
ProviderID.make(minimaxFixture.providerID),
|
||||
ModelID.make(model.id),
|
||||
ProviderV2.ID.make(minimaxFixture.providerID),
|
||||
ProviderV2.ModelID.make(model.id),
|
||||
)
|
||||
const sessionID = SessionID.make("session-test-3")
|
||||
const agent = {
|
||||
@@ -1538,8 +1540,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make("minimax"), modelID: ProviderV2.ModelID.make("MiniMax-M2.5") },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1615,7 +1617,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest("/messages", createEventResponse(chunks))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.make("anthropic"), ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make("anthropic"), ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-anthropic-tools")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1629,8 +1631,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make("anthropic"), modelID: resolved.id, variant: "max" },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
const input = [
|
||||
{
|
||||
@@ -1814,7 +1816,7 @@ describe("session.llm.stream", () => {
|
||||
]
|
||||
const request = waitRequest(pathSuffix, createEventResponse(chunks))
|
||||
|
||||
const resolved = yield* Provider.use.getModel(ProviderID.make(geminiFixture.providerID), ModelID.make(model.id))
|
||||
const resolved = yield* Provider.use.getModel(ProviderV2.ID.make(geminiFixture.providerID), ProviderV2.ModelID.make(model.id))
|
||||
const sessionID = SessionID.make("session-test-4")
|
||||
const agent = {
|
||||
name: "test",
|
||||
@@ -1831,8 +1833,8 @@ describe("session.llm.stream", () => {
|
||||
role: "user",
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(geminiFixture.providerID), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
model: { providerID: ProviderV2.ID.make(geminiFixture.providerID), modelID: resolved.id },
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { APICallError } from "ai"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { Question } from "../../src/question"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const sessionID = SessionID.make("session")
|
||||
const providerID = ProviderID.make("test")
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const model: Provider.Model = {
|
||||
id: ModelID.make("test-model"),
|
||||
id: ProviderV2.ModelID.make("test-model"),
|
||||
providerID,
|
||||
api: {
|
||||
id: "test-model",
|
||||
@@ -58,25 +60,25 @@ const model: Provider.Model = {
|
||||
release_date: "2026-01-01",
|
||||
}
|
||||
|
||||
function userInfo(id: string): MessageV2.User {
|
||||
function userInfo(id: string): SessionLegacy.User {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: 0 },
|
||||
agent: "user",
|
||||
model: { providerID, modelID: ModelID.make("test") },
|
||||
model: { providerID, modelID: ProviderV2.ModelID.make("test") },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.User
|
||||
} as unknown as SessionLegacy.User
|
||||
}
|
||||
|
||||
function assistantInfo(
|
||||
id: string,
|
||||
parentID: string,
|
||||
error?: MessageV2.Assistant["error"],
|
||||
error?: SessionLegacy.Assistant["error"],
|
||||
meta?: { providerID: string; modelID: string },
|
||||
): MessageV2.Assistant {
|
||||
): SessionLegacy.Assistant {
|
||||
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
|
||||
return {
|
||||
id,
|
||||
@@ -97,7 +99,7 @@ function assistantInfo(
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
} as unknown as MessageV2.Assistant
|
||||
} as unknown as SessionLegacy.Assistant
|
||||
}
|
||||
|
||||
function basePart(messageID: string, id: string) {
|
||||
@@ -110,7 +112,7 @@ function basePart(messageID: string, id: string) {
|
||||
|
||||
describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with no parts", async () => {
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo("m-empty"),
|
||||
parts: [],
|
||||
@@ -123,7 +125,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -138,7 +140,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with only ignored parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -148,7 +150,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "ignored",
|
||||
ignored: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -158,7 +160,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out user messages with only empty text parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -167,7 +169,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -177,7 +179,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters empty user text parts while keeping non-empty parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -191,7 +193,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -206,7 +208,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("includes synthetic text parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -216,7 +218,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "hello",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo("m-assistant", messageID),
|
||||
@@ -227,7 +229,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "assistant",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -246,7 +248,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("converts user text/file parts and injects compaction/subtask prompts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -294,7 +296,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
description: "desc",
|
||||
agent: "agent",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -320,7 +322,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -329,7 +331,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -364,7 +366,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -411,8 +413,8 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("preserves jpeg tool-result media for anthropic models", async () => {
|
||||
const anthropicModel: Provider.Model = {
|
||||
...model,
|
||||
id: ModelID.make("anthropic/claude-opus-4-7"),
|
||||
providerID: ProviderID.make("anthropic"),
|
||||
id: ProviderV2.ModelID.make("anthropic/claude-opus-4-7"),
|
||||
providerID: ProviderV2.ID.make("anthropic"),
|
||||
api: {
|
||||
id: "claude-opus-4-7-20250805",
|
||||
url: "https://api.anthropic.com",
|
||||
@@ -433,7 +435,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
)
|
||||
const userID = "m-user-anthropic"
|
||||
const assistantID = "m-assistant-anthropic"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -442,7 +444,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -470,7 +472,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -494,8 +496,8 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("moves bedrock pdf tool-result media into a separate user message", async () => {
|
||||
const bedrockModel: Provider.Model = {
|
||||
...model,
|
||||
id: ModelID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"),
|
||||
providerID: ProviderID.make("amazon-bedrock"),
|
||||
id: ProviderV2.ModelID.make("amazon-bedrock/anthropic.claude-sonnet-4-6"),
|
||||
providerID: ProviderV2.ID.make("amazon-bedrock"),
|
||||
api: {
|
||||
id: "anthropic.claude-sonnet-4-6",
|
||||
url: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
@@ -514,7 +516,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const pdf = Buffer.from("%PDF-1.4\n").toString("base64")
|
||||
const userID = "m-user-bedrock-pdf"
|
||||
const assistantID = "m-assistant-bedrock-pdf"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -523,7 +525,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -551,7 +553,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -602,7 +604,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -611,7 +613,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
|
||||
@@ -644,7 +646,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -685,7 +687,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -694,7 +696,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -713,7 +715,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1, compacted: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -752,7 +754,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -761,7 +763,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -780,7 +782,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -822,7 +824,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -831,7 +833,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -850,7 +852,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -900,7 +902,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
"</shell_metadata>",
|
||||
].join("\n")
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -909,7 +911,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -927,7 +929,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -965,12 +967,12 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters assistant messages with non-abort errors", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(
|
||||
assistantID,
|
||||
"m-parent",
|
||||
new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
|
||||
new SessionLegacy.APIError({ message: "boom", isRetryable: true }).toObject() as SessionLegacy.APIError,
|
||||
),
|
||||
parts: [
|
||||
{
|
||||
@@ -978,7 +980,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "should not render",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -989,9 +991,9 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const assistantID1 = "m-assistant-1"
|
||||
const assistantID2 = "m-assistant-2"
|
||||
|
||||
const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
|
||||
const aborted = new SessionLegacy.AbortedError({ message: "aborted" }).toObject() as SessionLegacy.Assistant["error"]
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID1, "m-parent", aborted),
|
||||
parts: [
|
||||
@@ -1006,7 +1008,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "partial answer",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID2, "m-parent", aborted),
|
||||
@@ -1021,7 +1023,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "thinking",
|
||||
time: { start: 0 },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1040,8 +1042,8 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const assistantID = "m-assistant"
|
||||
const openrouterModel: Provider.Model = {
|
||||
...model,
|
||||
id: ModelID.make("deepseek/deepseek-v4-pro"),
|
||||
providerID: ProviderID.make("openrouter"),
|
||||
id: ProviderV2.ModelID.make("deepseek/deepseek-v4-pro"),
|
||||
providerID: ProviderV2.ID.make("openrouter"),
|
||||
api: {
|
||||
id: "deepseek/deepseek-v4-pro",
|
||||
url: "https://openrouter.ai/api/v1",
|
||||
@@ -1061,7 +1063,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
index: 0,
|
||||
},
|
||||
]
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent", undefined, {
|
||||
providerID: openrouterModel.providerID,
|
||||
@@ -1084,7 +1086,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "answer",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1112,7 +1114,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("splits assistant messages on step-start boundaries", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1130,7 +1132,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "second",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1149,7 +1151,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("drops messages that only contain step-start parts", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1157,7 +1159,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
...basePart(assistantID, "p1"),
|
||||
type: "step-start",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1168,7 +1170,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -1177,7 +1179,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -1204,7 +1206,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1257,7 +1259,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("substitutes space for empty text between signed reasoning blocks", async () => {
|
||||
// Reproduces the bug pattern: [reasoning(sig), text(""), reasoning(sig), text(full)]
|
||||
const assistantID = "m-assistant"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1277,7 +1279,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
metadata: { anthropic: { signature: "sig2" } },
|
||||
},
|
||||
{ ...basePart(assistantID, "p6"), type: "text", text: "the answer" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1293,7 +1295,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
// Bedrock signed reasoning is preserved as reasoning metadata, but unlike the
|
||||
// direct Anthropic path we do not preserve empty text separators for Bedrock.
|
||||
const assistantID = "m-assistant-bedrock"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1305,7 +1307,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
|
||||
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1320,14 +1322,14 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
// Non-Anthropic providers' reasoning doesn't position-validate, so empty text
|
||||
// should be filtered normally rather than substituted.
|
||||
const assistantID = "m-assistant-unsigned"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
{ ...basePart(assistantID, "p1"), type: "reasoning", text: "thinking" },
|
||||
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
|
||||
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1340,13 +1342,13 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
|
||||
test("leaves empty text alone in assistant messages without reasoning", async () => {
|
||||
const assistantID = "m-assistant-no-reasoning"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
{ ...basePart(assistantID, "p1"), type: "text", text: "" },
|
||||
{ ...basePart(assistantID, "p2"), type: "text", text: "hello" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1458,7 +1460,7 @@ describe("session.message-v2.fromError", () => {
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1479,7 +1481,7 @@ describe("session.message-v2.fromError", () => {
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
})
|
||||
|
||||
test("does not classify 429 no body as context overflow", () => {
|
||||
@@ -1494,8 +1496,8 @@ describe("session.message-v2.fromError", () => {
|
||||
}),
|
||||
{ providerID },
|
||||
)
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false)
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(false)
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
})
|
||||
|
||||
test("serializes unknown inputs", () => {
|
||||
@@ -1530,9 +1532,9 @@ describe("session.message-v2.fromError", () => {
|
||||
|
||||
const result = MessageV2.fromError(zlibError, { providerID })
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
expect((result as MessageV2.APIError).data.isRetryable).toBe(true)
|
||||
expect((result as MessageV2.APIError).data.message).toInclude("decompression")
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
expect((result as SessionLegacy.APIError).data.isRetryable).toBe(true)
|
||||
expect((result as SessionLegacy.APIError).data.message).toInclude("decompression")
|
||||
})
|
||||
|
||||
test("classifies ZlibError as AbortedError when abort context is provided", () => {
|
||||
@@ -1556,21 +1558,21 @@ describe("session.message-v2.latest", () => {
|
||||
const CONTINUE_USER = MessageID.make("msg_005")
|
||||
const NEW_COMPACTION_USER = MessageID.make("msg_006")
|
||||
|
||||
const tailUser: MessageV2.WithParts = {
|
||||
const tailUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(TAIL_USER),
|
||||
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as MessageV2.Part[],
|
||||
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const overflowAssistant: MessageV2.WithParts = {
|
||||
const overflowAssistant: SessionLegacy.WithParts = {
|
||||
info: {
|
||||
...assistantInfo(OVERFLOW_ASSISTANT, TAIL_USER),
|
||||
finish: "tool-calls",
|
||||
tokens: { input: 280_000, output: 200, reasoning: 0, cache: { read: 0, write: 0 }, total: 280_200 },
|
||||
} as MessageV2.Assistant,
|
||||
} as SessionLegacy.Assistant,
|
||||
parts: [],
|
||||
}
|
||||
|
||||
const compactionUser: MessageV2.WithParts = {
|
||||
const compactionUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(COMPACTION_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1579,20 +1581,20 @@ describe("session.message-v2.latest", () => {
|
||||
auto: true,
|
||||
tail_start_id: TAIL_USER,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const summaryAssistant: MessageV2.WithParts = {
|
||||
const summaryAssistant: SessionLegacy.WithParts = {
|
||||
info: {
|
||||
...assistantInfo(SUMMARY_ASSISTANT, COMPACTION_USER),
|
||||
summary: true,
|
||||
finish: "stop",
|
||||
tokens: { input: 150_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 }, total: 151_500 },
|
||||
} as MessageV2.Assistant,
|
||||
} as SessionLegacy.Assistant,
|
||||
parts: [],
|
||||
}
|
||||
|
||||
const continueUser: MessageV2.WithParts = {
|
||||
const continueUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(CONTINUE_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1602,7 +1604,7 @@ describe("session.message-v2.latest", () => {
|
||||
synthetic: true,
|
||||
metadata: { compaction_continue: true },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
// Regression for double auto-compaction. The reorder in filterCompacted
|
||||
@@ -1628,7 +1630,7 @@ describe("session.message-v2.latest", () => {
|
||||
})
|
||||
|
||||
test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
|
||||
const newCompactionUser: MessageV2.WithParts = {
|
||||
const newCompactionUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(NEW_COMPACTION_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1636,7 +1638,7 @@ describe("session.message-v2.latest", () => {
|
||||
type: "compaction",
|
||||
auto: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const state = MessageV2.latest([
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Option } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const it = testEffect(SessionNs.defaultLayer)
|
||||
const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Database.defaultLayer))
|
||||
|
||||
const withSession = <A, E, R>(
|
||||
fn: (input: { session: SessionNs.Interface; sessionID: SessionID }) => Effect.Effect<A, E, R>,
|
||||
@@ -45,7 +48,7 @@ const fill = Effect.fn("Test.fill")(function* (
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
@@ -69,7 +72,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
if (text) {
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
@@ -85,7 +88,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
|
||||
const addAssistant = Effect.fn("Test.addAssistant")(function* (
|
||||
sessionID: SessionID,
|
||||
parentID: MessageID,
|
||||
opts?: { summary?: boolean; finish?: string; error?: MessageV2.Assistant["error"] },
|
||||
opts?: { summary?: boolean; finish?: string; error?: SessionLegacy.Assistant["error"] },
|
||||
) {
|
||||
const session = yield* SessionNs.Service
|
||||
const id = MessageID.ascending()
|
||||
@@ -95,8 +98,8 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
|
||||
role: "assistant",
|
||||
time: { created: Date.now() },
|
||||
parentID,
|
||||
modelID: ModelID.make("test"),
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
mode: "",
|
||||
agent: "default",
|
||||
path: { cwd: "/", root: "/" },
|
||||
@@ -105,7 +108,7 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
|
||||
summary: opts?.summary,
|
||||
finish: opts?.finish,
|
||||
error: opts?.error,
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
return id
|
||||
})
|
||||
|
||||
@@ -310,7 +313,7 @@ describe("MessageV2.stream", () => {
|
||||
Effect.gen(function* () {
|
||||
const ids = yield* fill(sessionID, 5)
|
||||
|
||||
const items = Array.from(MessageV2.stream(sessionID))
|
||||
const items = yield* MessageV2.stream(sessionID)
|
||||
expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse())
|
||||
}),
|
||||
),
|
||||
@@ -319,7 +322,7 @@ describe("MessageV2.stream", () => {
|
||||
it.instance("yields nothing for empty session", () =>
|
||||
withSession(({ sessionID }) =>
|
||||
Effect.gen(function* () {
|
||||
const items = Array.from(MessageV2.stream(sessionID))
|
||||
const items = yield* MessageV2.stream(sessionID)
|
||||
expect(items).toHaveLength(0)
|
||||
}),
|
||||
),
|
||||
@@ -330,7 +333,7 @@ describe("MessageV2.stream", () => {
|
||||
Effect.gen(function* () {
|
||||
const ids = yield* fill(sessionID, 1)
|
||||
|
||||
const items = Array.from(MessageV2.stream(sessionID))
|
||||
const items = yield* MessageV2.stream(sessionID)
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].info.id).toBe(ids[0])
|
||||
}),
|
||||
@@ -342,7 +345,7 @@ describe("MessageV2.stream", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* fill(sessionID, 3)
|
||||
|
||||
const items = Array.from(MessageV2.stream(sessionID))
|
||||
const items = yield* MessageV2.stream(sessionID)
|
||||
for (const item of items) {
|
||||
expect(item.parts).toHaveLength(1)
|
||||
expect(item.parts[0].type).toBe("text")
|
||||
@@ -356,7 +359,7 @@ describe("MessageV2.stream", () => {
|
||||
Effect.gen(function* () {
|
||||
const ids = yield* fill(sessionID, 60)
|
||||
|
||||
const items = Array.from(MessageV2.stream(sessionID))
|
||||
const items = yield* MessageV2.stream(sessionID)
|
||||
expect(items).toHaveLength(60)
|
||||
expect(items[0].info.id).toBe(ids[ids.length - 1])
|
||||
expect(items[59].info.id).toBe(ids[0])
|
||||
@@ -364,17 +367,13 @@ describe("MessageV2.stream", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.instance("is a sync generator", () =>
|
||||
it.instance("returns an Effect", () =>
|
||||
withSession(({ sessionID }) =>
|
||||
Effect.gen(function* () {
|
||||
yield* fill(sessionID, 1)
|
||||
|
||||
const gen = MessageV2.stream(sessionID)
|
||||
const first = gen.next()
|
||||
// sync generator returns { value, done } directly, not a Promise
|
||||
expect(first).toHaveProperty("value")
|
||||
expect(first).toHaveProperty("done")
|
||||
expect(first.done).toBe(false)
|
||||
const result = yield* MessageV2.stream(sessionID)
|
||||
expect(result).toHaveLength(1)
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -386,10 +385,10 @@ describe("MessageV2.parts", () => {
|
||||
Effect.gen(function* () {
|
||||
const [id] = yield* fill(sessionID, 1)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
const result = yield* MessageV2.parts(id)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -399,7 +398,7 @@ describe("MessageV2.parts", () => {
|
||||
Effect.gen(function* () {
|
||||
const id = yield* addUser(sessionID)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
const result = yield* MessageV2.parts(id)
|
||||
expect(result).toEqual([])
|
||||
}),
|
||||
),
|
||||
@@ -425,11 +424,11 @@ describe("MessageV2.parts", () => {
|
||||
text: "third",
|
||||
})
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
const result = yield* MessageV2.parts(id)
|
||||
expect(result).toHaveLength(3)
|
||||
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result[1] as MessageV2.TextPart).text).toBe("second")
|
||||
expect((result[2] as MessageV2.TextPart).text).toBe("third")
|
||||
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
|
||||
expect((result[1] as SessionLegacy.TextPart).text).toBe("second")
|
||||
expect((result[2] as SessionLegacy.TextPart).text).toBe("third")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -437,7 +436,7 @@ describe("MessageV2.parts", () => {
|
||||
it.instance("returns empty for non-existent message id", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* SessionNs.Service
|
||||
const result = MessageV2.parts(MessageID.ascending())
|
||||
const result = yield* MessageV2.parts(MessageID.ascending())
|
||||
expect(result).toEqual([])
|
||||
}),
|
||||
)
|
||||
@@ -447,7 +446,7 @@ describe("MessageV2.parts", () => {
|
||||
Effect.gen(function* () {
|
||||
const [id] = yield* fill(sessionID, 1)
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
const result = yield* MessageV2.parts(id)
|
||||
expect(result[0].sessionID).toBe(sessionID)
|
||||
expect(result[0].messageID).toBe(id)
|
||||
}),
|
||||
@@ -466,7 +465,7 @@ describe("MessageV2.get", () => {
|
||||
expect(result.info.sessionID).toBe(sessionID)
|
||||
expect(result.info.role).toBe("user")
|
||||
expect(result.parts).toHaveLength(1)
|
||||
expect((result.parts[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("m0")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -536,7 +535,7 @@ describe("MessageV2.get", () => {
|
||||
const result = yield* MessageV2.get({ sessionID, messageID: aid })
|
||||
expect(result.info.role).toBe("assistant")
|
||||
expect(result.parts).toHaveLength(1)
|
||||
expect((result.parts[0] as MessageV2.TextPart).text).toBe("response")
|
||||
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("response")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -604,7 +603,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
Effect.gen(function* () {
|
||||
const ids = yield* fill(sessionID, 5)
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
expect(result).toHaveLength(5)
|
||||
// reversed from newest-first to chronological
|
||||
expect(result.map((item) => item.info.id)).toEqual(ids)
|
||||
@@ -638,7 +637,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
text: "new response",
|
||||
})
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
// Includes compaction boundary: u1, a1, u2, a2
|
||||
expect(result[0].info.id).toBe(u1)
|
||||
expect(result.length).toBe(4)
|
||||
@@ -660,7 +659,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
yield* addCompactionPart(sessionID, u1)
|
||||
yield* addUser(sessionID, "world")
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
expect(result).toHaveLength(2)
|
||||
}),
|
||||
),
|
||||
@@ -672,14 +671,14 @@ describe("MessageV2.filterCompacted", () => {
|
||||
const u1 = yield* addUser(sessionID, "hello")
|
||||
yield* addCompactionPart(sessionID, u1)
|
||||
|
||||
const error = new MessageV2.APIError({
|
||||
const error = new SessionLegacy.APIError({
|
||||
message: "boom",
|
||||
isRetryable: true,
|
||||
}).toObject() as MessageV2.Assistant["error"]
|
||||
}).toObject() as SessionLegacy.Assistant["error"]
|
||||
yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error })
|
||||
yield* addUser(sessionID, "retry")
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
// Error assistant doesn't add to completed, so compaction boundary never triggers
|
||||
expect(result).toHaveLength(3)
|
||||
}),
|
||||
@@ -696,7 +695,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
yield* addAssistant(sessionID, u1, { summary: true })
|
||||
yield* addUser(sessionID, "next")
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
expect(result).toHaveLength(3)
|
||||
}),
|
||||
),
|
||||
@@ -746,7 +745,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
text: "third reply",
|
||||
})
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
|
||||
}),
|
||||
@@ -799,11 +798,11 @@ describe("MessageV2.filterCompacted", () => {
|
||||
text: "third reply",
|
||||
})
|
||||
|
||||
const parentFiltered = MessageV2.filterCompacted(MessageV2.stream(created.id))
|
||||
const parentFiltered = MessageV2.filterCompacted(yield* MessageV2.stream(created.id))
|
||||
expect(parentFiltered.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
|
||||
|
||||
const forked = yield* session.fork({ sessionID: created.id })
|
||||
const childFiltered = MessageV2.filterCompacted(MessageV2.stream(forked.id))
|
||||
const childFiltered = MessageV2.filterCompacted(yield* MessageV2.stream(forked.id))
|
||||
expect(childFiltered).toHaveLength(parentFiltered.length)
|
||||
|
||||
const tailPart = childFiltered.flatMap((m) => m.parts).find((p) => p.type === "compaction")
|
||||
@@ -869,7 +868,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
text: "third reply",
|
||||
})
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c1, s1, a3, u3, a4])
|
||||
}),
|
||||
@@ -941,7 +940,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
text: "fourth reply",
|
||||
})
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const result = MessageV2.filterCompacted(yield* MessageV2.stream(sessionID))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c2, s2, u3, a3, u4, a4])
|
||||
}),
|
||||
@@ -951,7 +950,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
test("works with array input", () => {
|
||||
// filterCompacted accepts any Iterable, not just generators
|
||||
const id = MessageID.ascending()
|
||||
const items: MessageV2.WithParts[] = [
|
||||
const items: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: {
|
||||
id,
|
||||
@@ -960,8 +959,8 @@ describe("MessageV2.filterCompacted", () => {
|
||||
time: { created: 1 },
|
||||
agent: "test",
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
} as unknown as MessageV2.Info,
|
||||
parts: [{ type: "text", text: "hello" }] as unknown as MessageV2.Part[],
|
||||
} as unknown as SessionLegacy.Info,
|
||||
parts: [{ type: "text", text: "hello" }] as unknown as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
const result = MessageV2.filterCompacted(items)
|
||||
@@ -1014,7 +1013,7 @@ describe("MessageV2 consistency", () => {
|
||||
const [id] = yield* fill(sessionID, 1)
|
||||
|
||||
const got = yield* MessageV2.get({ sessionID, messageID: id })
|
||||
const standalone = MessageV2.parts(id)
|
||||
const standalone = yield* MessageV2.parts(id)
|
||||
expect(got.parts).toEqual(standalone)
|
||||
}),
|
||||
),
|
||||
@@ -1025,9 +1024,9 @@ describe("MessageV2 consistency", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* fill(sessionID, 7)
|
||||
|
||||
const streamed = Array.from(MessageV2.stream(sessionID))
|
||||
const streamed = yield* MessageV2.stream(sessionID)
|
||||
|
||||
const paged = [] as MessageV2.WithParts[]
|
||||
const paged = [] as SessionLegacy.WithParts[]
|
||||
let cursor: string | undefined
|
||||
while (true) {
|
||||
const result = yield* MessageV2.page({ sessionID, limit: 3, before: cursor })
|
||||
@@ -1048,8 +1047,9 @@ describe("MessageV2 consistency", () => {
|
||||
Effect.gen(function* () {
|
||||
yield* fill(sessionID, 4)
|
||||
|
||||
const filtered = MessageV2.filterCompacted(MessageV2.stream(sessionID))
|
||||
const all = Array.from(MessageV2.stream(sessionID)).reverse()
|
||||
const stream = yield* MessageV2.stream(sessionID)
|
||||
const filtered = MessageV2.filterCompacted(stream)
|
||||
const all = stream.toReversed()
|
||||
|
||||
expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id))
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { expect } from "bun:test"
|
||||
import { tool } from "ai"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -6,13 +9,12 @@ import path from "path"
|
||||
import z from "zod"
|
||||
import type { Agent } from "../../src/agent/agent"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { Image } from "@/image/image"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Plugin } from "../../src/plugin"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -26,9 +28,8 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { raw, reply, TestLLMServer } from "../lib/llm-server"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -42,8 +43,8 @@ const summary = Layer.succeed(
|
||||
)
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ModelID.make("test-model"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
}
|
||||
|
||||
const cfg = {
|
||||
@@ -145,7 +146,7 @@ const assistant = Effect.fn("TestSession.assistant")(function* (
|
||||
root: string,
|
||||
) {
|
||||
const session = yield* Session.Service
|
||||
const msg: MessageV2.Assistant = {
|
||||
const msg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -170,7 +171,7 @@ const assistant = Effect.fn("TestSession.assistant")(function* (
|
||||
return msg
|
||||
})
|
||||
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer))
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||
const deps = Layer.mergeAll(
|
||||
Session.defaultLayer,
|
||||
@@ -182,7 +183,7 @@ const deps = Layer.mergeAll(
|
||||
LLM.defaultLayer,
|
||||
Provider.defaultLayer,
|
||||
status,
|
||||
SyncEvent.defaultLayer,
|
||||
Database.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const env = Layer.mergeAll(
|
||||
@@ -212,6 +213,7 @@ it.live("session.processor effect tests capture llm input cleanly", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.text("hello")
|
||||
@@ -234,7 +236,7 @@ it.live("session.processor effect tests capture llm input cleanly", () =>
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -244,7 +246,7 @@ it.live("session.processor effect tests capture llm input cleanly", () =>
|
||||
} satisfies LLM.StreamInput
|
||||
|
||||
const value = yield* handle.process(input)
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const calls = yield* llm.calls
|
||||
|
||||
expect(value).toBe("continue")
|
||||
@@ -259,6 +261,7 @@ it.live("session.processor effect tests preserve text start time", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const gate = defer<void>()
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
@@ -306,7 +309,7 @@ it.live("session.processor effect tests preserve text start time", () =>
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -317,14 +320,19 @@ it.live("session.processor effect tests preserve text start time", () =>
|
||||
.pipe(Effect.forkChild)
|
||||
|
||||
yield* waitFor(
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is MessageV2.TextPart => part.type === "text")),
|
||||
MessageV2.parts(msg.id).pipe(
|
||||
Effect.map((parts) => parts.find((part): part is SessionLegacy.TextPart => part.type === "text")),
|
||||
Effect.provideService(Database.Service, database),
|
||||
),
|
||||
"timed out waiting for text part",
|
||||
)
|
||||
yield* Effect.sleep("20 millis")
|
||||
gate.resolve()
|
||||
|
||||
const exit = yield* Fiber.await(run)
|
||||
const text = MessageV2.parts(msg.id).find((part): part is MessageV2.TextPart => part.type === "text")
|
||||
const text = (yield* MessageV2.parts(msg.id)).find(
|
||||
(part): part is SessionLegacy.TextPart => part.type === "text",
|
||||
)
|
||||
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
expect(text?.text).toBe("hello")
|
||||
@@ -341,6 +349,7 @@ it.live("session.processor effect tests stop after token overflow requests compa
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.text("after", { usage: { input: 100, output: 0 } })
|
||||
@@ -364,7 +373,7 @@ it.live("session.processor effect tests stop after token overflow requests compa
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -373,7 +382,7 @@ it.live("session.processor effect tests stop after token overflow requests compa
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
|
||||
expect(value).toBe("compact")
|
||||
expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
|
||||
@@ -387,6 +396,7 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.push(reply().reason("think").text("done").stop())
|
||||
@@ -409,7 +419,7 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -418,9 +428,9 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const reasoning = parts.find((part): part is MessageV2.ReasoningPart => part.type === "reasoning")
|
||||
const text = parts.find((part): part is MessageV2.TextPart => part.type === "text")
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const reasoning = parts.find((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
|
||||
const text = parts.find((part): part is SessionLegacy.TextPart => part.type === "text")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(1)
|
||||
@@ -457,7 +467,7 @@ it.live("session.processor effect tests reset reasoning state across retries", (
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -466,8 +476,8 @@ it.live("session.processor effect tests reset reasoning state across retries", (
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const reasoning = parts.filter((part): part is MessageV2.ReasoningPart => part.type === "reasoning")
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const reasoning = parts.filter((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
@@ -504,7 +514,7 @@ it.live("session.processor effect tests do not retry unknown json errors", () =>
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -548,7 +558,7 @@ it.live("session.processor effect tests retry recognized structured json errors"
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -557,7 +567,7 @@ it.live("session.processor effect tests retry recognized structured json errors"
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
@@ -573,7 +583,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
|
||||
yield* llm.error(503, { error: "boom" })
|
||||
yield* llm.text("")
|
||||
@@ -583,9 +593,11 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const states: number[] = []
|
||||
const off = yield* bus.subscribeCallback(SessionStatus.Event.Status, (evt) => {
|
||||
if (evt.properties.sessionID !== chat.id) return
|
||||
if (evt.properties.status.type === "retry") states.push(evt.properties.status.attempt)
|
||||
const off = yield* events.listen((evt) => {
|
||||
if (evt.type !== SessionStatus.Event.Status.type) return Effect.void
|
||||
const data = evt.data as typeof SessionStatus.Event.Status.data.Type
|
||||
if (data.sessionID === chat.id && data.status.type === "retry") states.push(data.status.attempt)
|
||||
return Effect.void
|
||||
})
|
||||
const handle = yield* processors.create({
|
||||
assistantMessage: msg,
|
||||
@@ -601,7 +613,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -610,7 +622,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
|
||||
tools: {},
|
||||
})
|
||||
|
||||
off()
|
||||
yield* off
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
@@ -646,7 +658,7 @@ it.live("session.processor effect tests compact on structured context overflow",
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -689,7 +701,7 @@ it.live("session.processor effect tests complete AI SDK tool calls when native f
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -708,8 +720,8 @@ it.live("session.processor effect tests complete AI SDK tool calls when native f
|
||||
},
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(1)
|
||||
@@ -732,6 +744,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const database = yield* Database.Service
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.toolHang("bash", { cmd: "pwd" })
|
||||
@@ -755,7 +768,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -767,14 +780,17 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
||||
|
||||
yield* llm.wait(1)
|
||||
yield* waitFor(
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is MessageV2.ToolPart => part.type === "tool")),
|
||||
MessageV2.parts(msg.id).pipe(
|
||||
Effect.map((parts) => parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")),
|
||||
Effect.provideService(Database.Service, database),
|
||||
),
|
||||
"timed out waiting for tool part",
|
||||
)
|
||||
yield* Fiber.interrupt(run)
|
||||
|
||||
const exit = yield* Fiber.await(run)
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
@@ -798,7 +814,7 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
||||
Effect.gen(function* () {
|
||||
const seen = defer<void>()
|
||||
const { processors, session, provider } = yield* boot()
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const sts = yield* SessionStatus.Service
|
||||
|
||||
yield* llm.hang
|
||||
@@ -808,11 +824,13 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const errs: string[] = []
|
||||
const off = yield* bus.subscribeCallback(Session.Event.Error, (evt) => {
|
||||
if (evt.properties.sessionID !== chat.id) return
|
||||
if (!evt.properties.error) return
|
||||
errs.push(evt.properties.error.name)
|
||||
const off = yield* events.listen((evt) => {
|
||||
if (evt.type !== Session.Event.Error.type) return Effect.void
|
||||
const data = evt.data as typeof Session.Event.Error.data.Type
|
||||
if (data.sessionID !== chat.id || !data.error) return Effect.void
|
||||
errs.push(data.error.name)
|
||||
seen.resolve()
|
||||
return Effect.void
|
||||
})
|
||||
const handle = yield* processors.create({
|
||||
assistantMessage: msg,
|
||||
@@ -829,7 +847,7 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -846,7 +864,7 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
||||
yield* Effect.promise(() => seen.promise)
|
||||
const stored = yield* MessageV2.get({ sessionID: chat.id, messageID: msg.id })
|
||||
const state = yield* sts.get(chat.id)
|
||||
off()
|
||||
yield* off
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
@@ -892,7 +910,7 @@ it.live("session.processor effect tests mark interruptions aborted without manua
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { expect } from "bun:test"
|
||||
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -7,7 +11,6 @@ import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Config } from "@/config/config"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
@@ -18,11 +21,11 @@ import { Provider as ProviderSvc } from "@/provider/provider"
|
||||
import { Env } from "../../src/env"
|
||||
import { Git } from "../../src/git"
|
||||
import { Image } from "../../src/image/image"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { Question } from "../../src/question"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionMessageTable } from "../../src/session/session.sql"
|
||||
import { SessionMessageTable } from "@opencode-ai/core/session/sql"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
@@ -35,7 +38,7 @@ import { SessionRevert } from "../../src/session/revert"
|
||||
import { SessionRunState } from "../../src/session/run-state"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionV2 } from "../../src/v2/session"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { Shell } from "../../src/shell/shell"
|
||||
@@ -44,7 +47,6 @@ import { ToolRegistry } from "@/tool/registry"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import * as Database from "../../src/storage/db"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { Format } from "../../src/format"
|
||||
import { Reference } from "../../src/reference/reference"
|
||||
@@ -52,9 +54,8 @@ import { RepositoryCache } from "../../src/reference/repository-cache"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
|
||||
import { reply, TestLLMServer } from "../lib/llm-server"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -68,8 +69,8 @@ const summary = Layer.succeed(
|
||||
)
|
||||
|
||||
const ref = {
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ModelID.make("test-model"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
}
|
||||
|
||||
function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
|
||||
@@ -90,20 +91,20 @@ function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
|
||||
)
|
||||
}
|
||||
|
||||
function toolPart(parts: MessageV2.Part[]) {
|
||||
return parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
function toolPart(parts: SessionLegacy.Part[]) {
|
||||
return parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
}
|
||||
|
||||
type CompletedToolPart = MessageV2.ToolPart & { state: MessageV2.ToolStateCompleted }
|
||||
type ErrorToolPart = MessageV2.ToolPart & { state: MessageV2.ToolStateError }
|
||||
type CompletedToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateCompleted }
|
||||
type ErrorToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateError }
|
||||
|
||||
function completedTool(parts: MessageV2.Part[]) {
|
||||
function completedTool(parts: SessionLegacy.Part[]) {
|
||||
const part = toolPart(parts)
|
||||
expect(part?.state.status).toBe("completed")
|
||||
return part?.state.status === "completed" ? (part as CompletedToolPart) : undefined
|
||||
}
|
||||
|
||||
function errorTool(parts: MessageV2.Part[]) {
|
||||
function errorTool(parts: SessionLegacy.Part[]) {
|
||||
const part = toolPart(parts)
|
||||
expect(part?.state.status).toBe("error")
|
||||
return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
|
||||
@@ -152,7 +153,7 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer))
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
@@ -181,7 +182,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
|
||||
AppFileSystem.defaultLayer,
|
||||
BackgroundJob.defaultLayer,
|
||||
status,
|
||||
SyncEvent.defaultLayer,
|
||||
Database.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
@@ -388,7 +389,7 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: strin
|
||||
const seed = Effect.fn("test.seed")(function* (sessionID: SessionID, opts?: { finish?: string }) {
|
||||
const session = yield* Session.Service
|
||||
const msg = yield* user(sessionID, "hello")
|
||||
const assistant: MessageV2.Assistant = {
|
||||
const assistant: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: msg.id,
|
||||
@@ -511,8 +512,8 @@ it.instance("loop calls LLM and returns assistant message", () =>
|
||||
}),
|
||||
)
|
||||
|
||||
noLLMServer.instance(
|
||||
"prompt emits v2 prompted and synthetic events",
|
||||
noLLMServer.instance.skip(
|
||||
"prompt emits v2 prompted and synthetic events (v2 projector disabled)",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
@@ -535,11 +536,10 @@ noLLMServer.instance(
|
||||
})
|
||||
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(SessionV2.layer),
|
||||
)
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(),
|
||||
Effect.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
const { db } = yield* Database.Service
|
||||
const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.session_id, chat.id)).get().pipe(Effect.orDie)
|
||||
expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
|
||||
expect(typeof row?.data.time.created).toBe("number")
|
||||
expect(messages).toEqual(
|
||||
@@ -753,8 +753,8 @@ it.instance("failed subtask preserves metadata on error tool state", () =>
|
||||
expect(tool.state.metadata).toBeDefined()
|
||||
expect(tool.state.metadata?.sessionId).toBeDefined()
|
||||
expect(tool.state.metadata?.model).toEqual({
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ModelID.make("missing-model"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("missing-model"),
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -777,7 +777,7 @@ it.instance(
|
||||
Effect.gen(function* () {
|
||||
const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
|
||||
const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
|
||||
const tool = taskMsg?.parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
const tool = taskMsg?.parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
|
||||
}),
|
||||
"timed out waiting for running subtask metadata",
|
||||
@@ -820,7 +820,7 @@ it.instance(
|
||||
const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
|
||||
const assistant = msgs.findLast((item) => item.info.role === "assistant" && item.info.agent === "build")
|
||||
const tool = assistant?.parts.find(
|
||||
(part): part is MessageV2.ToolPart => part.type === "tool" && part.tool === "task",
|
||||
(part): part is SessionLegacy.ToolPart => part.type === "tool" && part.tool === "task",
|
||||
)
|
||||
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
|
||||
}),
|
||||
@@ -1364,24 +1364,26 @@ unixNoLLMServer(
|
||||
unixNoLLMServer(
|
||||
"shell commands can change directory after startup",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const { prompt, run, chat } = yield* boot()
|
||||
const parent = path.dirname(dir)
|
||||
const result = yield* prompt.shell({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
command: "cd .. && pwd",
|
||||
})
|
||||
withSh(() =>
|
||||
Effect.gen(function* () {
|
||||
const { directory: dir } = yield* TestInstance
|
||||
const { prompt, run, chat } = yield* boot()
|
||||
const parent = path.dirname(dir)
|
||||
const result = yield* prompt.shell({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
command: "cd .. && pwd",
|
||||
})
|
||||
|
||||
expect(result.info.role).toBe("assistant")
|
||||
const tool = completedTool(result.parts)
|
||||
if (!tool) return
|
||||
expect(result.info.role).toBe("assistant")
|
||||
const tool = completedTool(result.parts)
|
||||
if (!tool) return
|
||||
|
||||
expect(tool.state.output).toContain(parent)
|
||||
expect(tool.state.metadata.output).toContain(parent)
|
||||
yield* run.assertNotBusy(chat.id)
|
||||
}),
|
||||
expect(tool.state.output).toContain(parent)
|
||||
expect(tool.state.metadata.output).toContain(parent)
|
||||
yield* run.assertNotBusy(chat.id)
|
||||
}),
|
||||
),
|
||||
{ config: cfg },
|
||||
)
|
||||
|
||||
@@ -1939,11 +1941,11 @@ noLLMServer.instance(
|
||||
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
|
||||
)
|
||||
const references = parts.filter(
|
||||
(part): part is MessageV2.TextPartInput =>
|
||||
(part): part is SessionLegacy.TextPartInput =>
|
||||
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
|
||||
)
|
||||
const files = parts.filter((part): part is MessageV2.FilePartInput => part.type === "file")
|
||||
const agents = parts.filter((part): part is MessageV2.AgentPartInput => part.type === "agent")
|
||||
const files = parts.filter((part): part is SessionLegacy.FilePartInput => part.type === "file")
|
||||
const agents = parts.filter((part): part is SessionLegacy.AgentPartInput => part.type === "agent")
|
||||
const bare = references.find((part) => part.text.includes("@docs."))
|
||||
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
|
||||
const guide = files.find((part) => part.filename === "docs/guide")
|
||||
@@ -1996,7 +1998,7 @@ noLLMServer.instance(
|
||||
|
||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const synthetic = stored.parts.filter(
|
||||
(part): part is MessageV2.TextPart => part.type === "text" && part.synthetic === true,
|
||||
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
|
||||
)
|
||||
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
|
||||
|
||||
@@ -2051,7 +2053,7 @@ noLLMServer.instance(
|
||||
|
||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const synthetic = stored.parts.filter(
|
||||
(part): part is MessageV2.TextPart => part.type === "text" && part.synthetic === true,
|
||||
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
|
||||
)
|
||||
const reference = synthetic.find((part) =>
|
||||
part.text.startsWith("Referenced configured reference @docs/README.md."),
|
||||
@@ -2198,7 +2200,7 @@ noLLMServer.instance(
|
||||
const other = yield* prompt.prompt({
|
||||
sessionID: session.id,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") },
|
||||
model: { providerID: ProviderV2.ID.make("opencode"), modelID: ProviderV2.ModelID.make("kimi-k2.5-free") },
|
||||
noReply: true,
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
})
|
||||
@@ -2213,8 +2215,8 @@ noLLMServer.instance(
|
||||
})
|
||||
if (match.info.role !== "user") throw new Error("expected user message")
|
||||
expect(match.info.model).toEqual({
|
||||
providerID: ProviderID.make("test"),
|
||||
modelID: ModelID.make("test-model"),
|
||||
providerID: ProviderV2.ID.make("test"),
|
||||
modelID: ProviderV2.ModelID.make("test-model"),
|
||||
variant: "xhigh",
|
||||
})
|
||||
expect(match.info.model.variant).toBe("xhigh")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { APICallError } from "ai"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
@@ -6,20 +7,19 @@ import { Effect, Layer, Schedule, Schema } from "effect"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { SessionRetry } from "../../src/session/retry"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { ProviderID } from "../../src/provider/schema"
|
||||
import { ProviderError } from "../../src/provider/error"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const providerID = ProviderID.make("test")
|
||||
const providerID = ProviderV2.ID.make("test")
|
||||
const retryProvider = "test"
|
||||
const it = testEffect(Layer.mergeAll(SessionStatus.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
function apiError(headers?: Record<string, string>): MessageV2.APIError {
|
||||
return Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
function apiError(headers?: Record<string, string>): SessionLegacy.APIError {
|
||||
return Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "boom",
|
||||
isRetryable: true,
|
||||
responseHeaders: headers,
|
||||
@@ -85,9 +85,8 @@ describe("session.retry.delay", () => {
|
||||
expect(SessionRetry.delay(1, error)).toBe(SessionRetry.RETRY_MAX_DELAY)
|
||||
})
|
||||
|
||||
it.live("policy updates retry status and increments attempts", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
Effect.gen(function* () {
|
||||
it.instance("policy updates retry status and increments attempts", () =>
|
||||
Effect.gen(function* () {
|
||||
const sessionID = SessionID.make("session-retry-test")
|
||||
const error = apiError({ "retry-after-ms": "0" })
|
||||
const status = yield* SessionStatus.Service
|
||||
@@ -95,7 +94,7 @@ describe("session.retry.delay", () => {
|
||||
const step = yield* Schedule.toStepWithMetadata(
|
||||
SessionRetry.policy({
|
||||
provider: "test",
|
||||
parse: Schema.decodeUnknownSync(MessageV2.APIError.Schema),
|
||||
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
|
||||
set: (info) =>
|
||||
status.set(sessionID, {
|
||||
type: "retry",
|
||||
@@ -113,8 +112,7 @@ describe("session.retry.delay", () => {
|
||||
attempt: 2,
|
||||
message: "boom",
|
||||
})
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -166,7 +164,7 @@ describe("session.retry.retryable", () => {
|
||||
|
||||
test("retries transport timeout errors", () => {
|
||||
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
|
||||
expect(MessageV2.APIError.isInstance(request)).toBe(true)
|
||||
expect(SessionLegacy.APIError.isInstance(request)).toBe(true)
|
||||
expect(SessionRetry.retryable(request, retryProvider)).toEqual({
|
||||
message: "Provider response headers timed out after 10000ms",
|
||||
})
|
||||
@@ -177,14 +175,14 @@ describe("session.retry.retryable", () => {
|
||||
new ProviderError.ResponseStreamError("WebSocket closed before response.completed (code 1006: Connection ended)"),
|
||||
{ providerID },
|
||||
)
|
||||
expect(MessageV2.APIError.isInstance(request)).toBe(true)
|
||||
expect(SessionLegacy.APIError.isInstance(request)).toBe(true)
|
||||
expect(SessionRetry.retryable(request, retryProvider)).toEqual({
|
||||
message: "WebSocket closed before response.completed (code 1006: Connection ended)",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not retry context overflow errors", () => {
|
||||
const error = new MessageV2.ContextOverflowError({
|
||||
const error = new SessionLegacy.ContextOverflowError({
|
||||
message: "Input exceeds context window of this model",
|
||||
responseBody: '{"error":{"code":"context_length_exceeded"}}',
|
||||
}).toObject()
|
||||
@@ -193,8 +191,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 500 errors even when isRetryable is false", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Internal server error",
|
||||
isRetryable: false,
|
||||
statusCode: 500,
|
||||
@@ -206,8 +204,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 502 bad gateway errors", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Bad gateway",
|
||||
isRetryable: false,
|
||||
statusCode: 502,
|
||||
@@ -218,8 +216,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 503 service unavailable errors", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Service unavailable",
|
||||
isRetryable: false,
|
||||
statusCode: 503,
|
||||
@@ -230,8 +228,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("does not retry 4xx errors when isRetryable is false", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Bad request",
|
||||
isRetryable: false,
|
||||
statusCode: 400,
|
||||
@@ -242,8 +240,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries ZlibError decompression failures", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Response decompression failed",
|
||||
isRetryable: true,
|
||||
metadata: { code: "ZlibError" },
|
||||
@@ -256,8 +254,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps free limits to Go upsell action", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Free usage exceeded",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -282,8 +280,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps Go subscription limits to workspace PAYG upsell", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Subscription quota exceeded. You can continue using free models.",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -320,8 +318,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps Go subscription limits without limit metadata", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Subscription quota exceeded. You can continue using free models.",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -375,8 +373,8 @@ describe("session.message-v2.fromError", () => {
|
||||
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(result.data.isRetryable).toBe(true)
|
||||
expect(result.data.message).toBe("Connection reset by server")
|
||||
expect(result.data.metadata?.code).toBe("ECONNRESET")
|
||||
@@ -386,8 +384,8 @@ describe("session.message-v2.fromError", () => {
|
||||
)
|
||||
|
||||
test("ECONNRESET socket error is retryable", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Connection reset by server",
|
||||
isRetryable: true,
|
||||
metadata: { code: "ECONNRESET", message: "The socket connection was closed unexpectedly" },
|
||||
@@ -409,8 +407,8 @@ describe("session.message-v2.fromError", () => {
|
||||
responseBody: '{"error":"boom"}',
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID: ProviderID.make("openai") })
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("openai") })
|
||||
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(result.data.isRetryable).toBe(true)
|
||||
})
|
||||
|
||||
@@ -428,11 +426,11 @@ describe("session.message-v2.fromError", () => {
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ providerID: ProviderID.make("openai") },
|
||||
{ providerID: ProviderV2.ID.make("openai") },
|
||||
)
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(result.data.isRetryable).toBe(true)
|
||||
expect(SessionRetry.retryable(result, retryProvider)).toEqual({
|
||||
message: "An error occurred while processing your request.",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Session } from "@/session/session"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
|
||||
import { SessionRevert } from "../../src/session/revert"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Snapshot } from "../../src/snapshot"
|
||||
@@ -12,6 +13,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -31,7 +33,7 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "de
|
||||
role: "user" as const,
|
||||
sessionID,
|
||||
agent,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: ModelID.make("gpt-4") },
|
||||
model: { providerID: ProviderV2.ID.make("openai"), modelID: ProviderV2.ModelID.make("gpt-4") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
})
|
||||
@@ -47,8 +49,8 @@ const assistant = Effect.fn("test.assistant")(function* (sessionID: SessionID, p
|
||||
path: { cwd: dir, root: dir },
|
||||
cost: 0,
|
||||
tokens: { output: 0, input: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID,
|
||||
time: { created: Date.now() },
|
||||
finish: "end_turn",
|
||||
@@ -114,8 +116,8 @@ describe("revert + compact workflow", () => {
|
||||
sessionID,
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -130,7 +132,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "Hello, please help me",
|
||||
})
|
||||
|
||||
const assistantMsg1: MessageV2.Assistant = {
|
||||
const assistantMsg1: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -147,8 +149,8 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg1.id,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -171,8 +173,8 @@ describe("revert + compact workflow", () => {
|
||||
sessionID,
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -187,7 +189,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "What's the capital of France?",
|
||||
})
|
||||
|
||||
const assistantMsg2: MessageV2.Assistant = {
|
||||
const assistantMsg2: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -204,8 +206,8 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg2.id,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -276,8 +278,8 @@ describe("revert + compact workflow", () => {
|
||||
sessionID,
|
||||
agent: "default",
|
||||
model: {
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
},
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -292,7 +294,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "Hello",
|
||||
})
|
||||
|
||||
const assistantMsg: MessageV2.Assistant = {
|
||||
const assistantMsg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -309,8 +311,8 @@ describe("revert + compact workflow", () => {
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
modelID: ModelID.make("gpt-4"),
|
||||
providerID: ProviderID.make("openai"),
|
||||
modelID: ProviderV2.ModelID.make("gpt-4"),
|
||||
providerID: ProviderV2.ID.make("openai"),
|
||||
parentID: userMsg.id,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
|
||||
@@ -8,8 +8,8 @@ import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { SessionID, MessageID, PartID } from "../../src/session/schema"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
|
||||
// Covers the session-domain Effect Schema migration. For each migrated
|
||||
// schema we assert:
|
||||
@@ -22,8 +22,8 @@ const sessionID = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3
|
||||
const sessionIDChild = Schema.decodeUnknownSync(SessionID)("ses_01J5Y5H0AH4Q4NXJ6P4C3P5V2L")
|
||||
const messageID = Schema.decodeUnknownSync(MessageID)("msg_01J5Y5H0AH4Q4NXJ6P4C3P5V2M")
|
||||
const partID = Schema.decodeUnknownSync(PartID)("prt_01J5Y5H0AH4Q4NXJ6P4C3P5V2N")
|
||||
const projectID = ProjectID.make("proj-alpha")
|
||||
const workspaceID = Schema.decodeUnknownSync(WorkspaceID)("wrk-primary")
|
||||
const projectID = ProjectV2.ID.make("proj-alpha")
|
||||
const workspaceID = Schema.decodeUnknownSync(WorkspaceV2.ID)("wrk-primary")
|
||||
|
||||
function decodeUnknown<S extends Schema.Top>(schema: S) {
|
||||
const decode = Schema.decodeUnknownSync(schema as any)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { MessageID, SessionID } from "../../src/session/schema"
|
||||
import { Session } from "../../src/session/session"
|
||||
|
||||
const info = {
|
||||
id: SessionID.descending(),
|
||||
slug: "test-session",
|
||||
projectID: ProjectID.global,
|
||||
projectID: ProjectV2.ID.global,
|
||||
workspaceID: undefined,
|
||||
directory: "/tmp/opencode",
|
||||
parentID: undefined,
|
||||
@@ -43,7 +43,7 @@ describe("Session schema", () => {
|
||||
const encoded = Schema.encodeUnknownSync(Session.GlobalInfo)({
|
||||
...info,
|
||||
project: {
|
||||
id: ProjectID.global,
|
||||
id: ProjectV2.ID.global,
|
||||
name: undefined,
|
||||
worktree: "/tmp/opencode",
|
||||
},
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { Deferred, Effect, Exit, Layer } from "effect"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { provideInstance, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Bus } from "@/bus"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
SessionNs.layer.pipe(
|
||||
Layer.provide(Bus.layer),
|
||||
Layer.provide(Storage.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(SessionProjector.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: false })),
|
||||
Layer.provide(BackgroundJob.defaultLayer),
|
||||
),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
testInstanceStoreLayer,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -37,24 +40,19 @@ const awaitDeferred = <T>(deferred: Deferred.Deferred<T>, message: string) =>
|
||||
|
||||
const remove = (id: SessionID) => SessionNs.use.remove(id)
|
||||
|
||||
const subscribeGlobal = (type: string, callback: (event: NonNullable<GlobalEvent["payload"]>) => void) => {
|
||||
const listener = (event: GlobalEvent) => {
|
||||
if (event.payload?.type === type) callback(event.payload)
|
||||
}
|
||||
GlobalBus.on("event", listener)
|
||||
return () => GlobalBus.off("event", listener)
|
||||
}
|
||||
|
||||
describe("session.created event", () => {
|
||||
it.instance("should emit session.created event when session is created", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionNs.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const received = yield* Deferred.make<SessionNs.Info>()
|
||||
|
||||
const unsub = subscribeGlobal(SessionNs.Event.Created.type, (event) => {
|
||||
Deferred.doneUnsafe(received, Effect.succeed(event.properties.info as SessionNs.Info))
|
||||
const unsub = yield* events.listen((event) => {
|
||||
if (event.type === SessionNs.Event.Created.type)
|
||||
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof SessionNs.Event.Created.data.Type).info as SessionNs.Info))
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsub))
|
||||
yield* Effect.addFinalizer(() => unsub)
|
||||
|
||||
const info = yield* session.create({})
|
||||
const receivedInfo = yield* awaitDeferred(received, "timed out waiting for session.created")
|
||||
@@ -72,6 +70,7 @@ describe("session.created event", () => {
|
||||
it.instance("session.created event should be emitted before session.updated", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionNs.Service
|
||||
const source = yield* EventV2Bridge.Service
|
||||
const events: string[] = []
|
||||
const received = yield* Deferred.make<string[]>()
|
||||
const push = (event: string) => {
|
||||
@@ -81,17 +80,15 @@ describe("session.created event", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const unsubCreated = subscribeGlobal(SessionNs.Event.Created.type, () => {
|
||||
push("created")
|
||||
const unsubscribe = yield* source.listen((event) => {
|
||||
if (event.type === SessionNs.Event.Created.type) push("created")
|
||||
if (event.type === SessionNs.Event.Updated.type) push("updated")
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsubCreated))
|
||||
|
||||
const unsubUpdated = subscribeGlobal(SessionNs.Event.Updated.type, () => {
|
||||
push("updated")
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsubUpdated))
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
|
||||
const info = yield* session.create({})
|
||||
yield* session.setTitle({ sessionID: info.id, title: "updated" })
|
||||
const receivedEvents = yield* awaitDeferred(received, "timed out waiting for session created/updated events")
|
||||
|
||||
expect(receivedEvents).toContain("created")
|
||||
@@ -103,12 +100,13 @@ describe("session.created event", () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe("step-finish token propagation via Bus event", () => {
|
||||
describe("step-finish token propagation via event", () => {
|
||||
it.instance(
|
||||
"non-zero tokens propagate through PartUpdated event",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionNs.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const info = yield* session.create({})
|
||||
|
||||
const messageID = MessageID.ascending()
|
||||
@@ -121,16 +119,18 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
|
||||
// Bus subscribers receive readonly Schema.Type payloads; `MessageV2.Part`
|
||||
// Event subscribers receive readonly Schema.Type payloads; `SessionLegacy.Part`
|
||||
// is the mutable domain type. Cast bridges the two — safe because the
|
||||
// test only reads the value afterwards.
|
||||
const received = yield* Deferred.make<MessageV2.Part>()
|
||||
const unsub = subscribeGlobal(MessageV2.Event.PartUpdated.type, (event) => {
|
||||
Deferred.doneUnsafe(received, Effect.succeed(event.properties.part as MessageV2.Part))
|
||||
const received = yield* Deferred.make<SessionLegacy.Part>()
|
||||
const unsub = yield* events.listen((event) => {
|
||||
if (event.type === MessageV2.Event.PartUpdated.type)
|
||||
Deferred.doneUnsafe(received, Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part))
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsub))
|
||||
yield* Effect.addFinalizer(() => unsub)
|
||||
|
||||
const tokens = {
|
||||
total: 1500,
|
||||
@@ -154,7 +154,7 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
const receivedPart = yield* awaitDeferred(received, "timed out waiting for message.part.updated")
|
||||
|
||||
expect(receivedPart.type).toBe("step-finish")
|
||||
const finish = receivedPart as MessageV2.StepFinishPart
|
||||
const finish = receivedPart as SessionLegacy.StepFinishPart
|
||||
expect(finish.tokens.input).toBe(500)
|
||||
expect(finish.tokens.output).toBe(800)
|
||||
expect(finish.tokens.reasoning).toBe(200)
|
||||
|
||||
@@ -22,6 +22,7 @@ import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SessionRevert } from "../../src/session/revert"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -29,10 +30,11 @@ import { TestLLMServer } from "../lib/llm-server"
|
||||
|
||||
// Same layer setup as prompt-effect.test.ts
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Git } from "../../src/git"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { Command } from "../../src/command"
|
||||
import { Config } from "@/config/config"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
@@ -60,9 +62,7 @@ import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { Format } from "../../src/format"
|
||||
import { Reference } from "../../src/reference/reference"
|
||||
import { RepositoryCache } from "../../src/reference/repository-cache"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
@@ -109,7 +109,7 @@ const lsp = Layer.succeed(
|
||||
}),
|
||||
)
|
||||
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(Bus.layer))
|
||||
const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer))
|
||||
const run = SessionRunState.layer.pipe(Layer.provide(status))
|
||||
const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer)
|
||||
|
||||
@@ -130,7 +130,7 @@ function makeHttp() {
|
||||
AppFileSystem.defaultLayer,
|
||||
BackgroundJob.defaultLayer,
|
||||
status,
|
||||
SyncEvent.defaultLayer,
|
||||
Database.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
).pipe(Layer.provideMerge(infra))
|
||||
const question = Question.layer.pipe(Layer.provideMerge(deps))
|
||||
@@ -259,7 +259,7 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
||||
const allMsgs = yield* MessageV2.filterCompactedEffect(session.id)
|
||||
const tool = allMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.tool === "bash")
|
||||
.find((p): p is SessionLegacy.ToolPart => p.type === "tool" && p.tool === "bash")
|
||||
expect(tool?.state.status).toBe("completed")
|
||||
|
||||
// Poll for diff — summarize() is fire-and-forget
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
@@ -218,7 +219,7 @@ describe("StructuredOutput Integration", () => {
|
||||
)
|
||||
|
||||
test("unit test: StructuredOutputError is properly structured", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Failed to produce valid structured output after 3 attempts",
|
||||
retries: 3,
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Exit, Schema } from "effect"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SessionID, MessageID } from "../../src/session/schema"
|
||||
|
||||
const decodeFormat = Schema.decodeUnknownExit(MessageV2.Format)
|
||||
const decodeUser = Schema.decodeUnknownExit(MessageV2.User)
|
||||
const decodeAssistant = Schema.decodeUnknownExit(MessageV2.Assistant)
|
||||
const decodeFormat = Schema.decodeUnknownExit(SessionLegacy.Format)
|
||||
const decodeUser = Schema.decodeUnknownExit(SessionLegacy.User)
|
||||
const decodeAssistant = Schema.decodeUnknownExit(SessionLegacy.Assistant)
|
||||
|
||||
describe("structured-output.OutputFormat", () => {
|
||||
test("parses text format", () => {
|
||||
@@ -65,7 +66,7 @@ describe("structured-output.OutputFormat", () => {
|
||||
|
||||
describe("structured-output.StructuredOutputError", () => {
|
||||
test("creates error with message and retries", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Failed to validate",
|
||||
retries: 3,
|
||||
})
|
||||
@@ -76,7 +77,7 @@ describe("structured-output.StructuredOutputError", () => {
|
||||
})
|
||||
|
||||
test("converts to object correctly", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Test error",
|
||||
retries: 2,
|
||||
})
|
||||
@@ -88,13 +89,13 @@ describe("structured-output.StructuredOutputError", () => {
|
||||
})
|
||||
|
||||
test("isInstance correctly identifies error", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Test",
|
||||
retries: 1,
|
||||
})
|
||||
|
||||
expect(MessageV2.StructuredOutputError.isInstance(error)).toBe(true)
|
||||
expect(MessageV2.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
|
||||
expect(SessionLegacy.StructuredOutputError.isInstance(error)).toBe(true)
|
||||
expect(SessionLegacy.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user