chore: merge dev

This commit is contained in:
Dax Raad
2026-06-04 01:24:55 -04:00
312 changed files with 32760 additions and 4465 deletions
@@ -4,7 +4,7 @@ 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"
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
import path from "path"
import z from "zod"
import type { Agent } from "../../src/agent/agent"
@@ -25,11 +25,14 @@ import { SessionSummary } from "../../src/session/summary"
import { Snapshot } from "../../src/snapshot"
import * as Log from "@opencode-ai/core/util/log"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirServer } from "../fixture/fixture"
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { raw, reply, TestLLMServer } from "../lib/llm-server"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { LLMEvent } from "@opencode-ai/llm"
void Log.init({ print: false })
@@ -44,7 +47,7 @@ const summary = Layer.succeed(
const ref = {
providerID: ProviderV2.ID.make("test"),
modelID: ProviderV2.ModelID.make("test-model"),
modelID: ModelV2.ID.make("test-model"),
}
const cfg = {
@@ -198,6 +201,58 @@ const env = Layer.mergeAll(
const it = testEffect(env)
const providerErrorLLM = Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () =>
Stream.make(
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-1", name: "lookup" }),
LLMEvent.toolInputEnd({ id: "call-1", name: "lookup" }),
LLMEvent.toolCall({ id: "call-1", name: "lookup", input: {}, providerExecuted: true }),
LLMEvent.toolResult({
id: "call-1",
name: "lookup",
result: { type: "error", value: "provider boom" },
providerExecuted: true,
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
),
}),
)
const providerErrorEnv = SessionProcessor.layer.pipe(
Layer.provide(summary),
Layer.provide(Image.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provide(providerErrorLLM),
Layer.provideMerge(deps),
)
const itProviderError = testEffect(providerErrorEnv)
const fragmentFailureLLM = Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () =>
Stream.make(
LLMEvent.stepStart({ index: 0 }),
LLMEvent.reasoningStart({ id: "reasoning-1" }),
LLMEvent.reasoningDelta({ id: "reasoning-1", text: "thinking" }),
LLMEvent.textStart({ id: "text-1" }),
LLMEvent.textDelta({ id: "text-1", text: "partial" }),
LLMEvent.providerError({ message: "provider boom" }),
),
}),
)
const fragmentFailureEnv = SessionProcessor.layer.pipe(
Layer.provide(summary),
Layer.provide(Image.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provide(fragmentFailureLLM),
Layer.provideMerge(deps),
)
const itFragmentFailure = testEffect(fragmentFailureEnv)
const boot = Effect.fn("test.boot")(function* () {
const processors = yield* SessionProcessor.Service
const session = yield* Session.Service
@@ -936,3 +991,111 @@ it.live("session.processor effect tests mark interruptions aborted without manua
{ config: (url) => providerCfg(url) },
),
)
itProviderError.live("session.processor effect tests fail provider-executed error results", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
const events = yield* EventV2Bridge.Service
const chat = yield* session.create({})
const parent = yield* user(chat.id, "provider tool error")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
const off = yield* events.listen((event) => {
if (event.type === SessionEvent.Tool.Failed.type)
settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
return Effect.void
})
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "provider tool error" }],
tools: {},
})
yield* off
const parts = yield* MessageV2.parts(msg.id)
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
expect(call?.state.status).toBe("error")
if (call?.state.status === "error") expect(call.state.error).toBe("provider boom")
expect(settlements).toHaveLength(1)
expect(settlements[0]?.data).toMatchObject({
callID: "call-1",
error: { type: "unknown", message: "provider boom" },
result: { type: "error", value: "provider boom" },
provider: { executed: true },
})
}),
{ config: cfg },
),
)
itFragmentFailure.live("session.processor effect tests flush partial v2 fragments before step failure", () =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const { processors, session, provider } = yield* boot()
const events = yield* EventV2Bridge.Service
const chat = yield* session.create({})
const parent = yield* user(chat.id, "provider failure")
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
const seen: string[] = []
let text: string | undefined
let reasoning: string | undefined
const off = yield* events.listen((event) => {
seen.push(event.type)
if (event.type === SessionEvent.Text.Ended.type)
text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
if (event.type === SessionEvent.Reasoning.Ended.type)
reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
return Effect.void
})
const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
expect(
yield* handle.process({
user: {
id: parent.id,
sessionID: chat.id,
role: "user",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
system: [],
messages: [{ role: "user", content: "provider failure" }],
tools: {},
}),
).toBe("stop")
yield* off
const failed = seen.indexOf(SessionEvent.Step.Failed.type)
expect(failed).toBeGreaterThan(-1)
expect(seen.indexOf(SessionEvent.Text.Ended.type)).toBeLessThan(failed)
expect(seen.indexOf(SessionEvent.Reasoning.Ended.type)).toBeLessThan(failed)
expect(text).toBe("partial")
expect(reasoning).toBe("thinking")
}),
{ config: cfg },
),
)