chore: effect pattern lint infrastructure (#35210)

This commit is contained in:
Kit Langton 2026-07-03 13:58:26 -04:00 committed by GitHub
commit 1401901529
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 524 additions and 234 deletions

View file

@ -2,10 +2,8 @@ export * as SessionCompaction from "./compaction"
import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/llm"
import { Context, DateTime, Effect, Layer, Stream } from "effect"
import type { Config } from "../config"
import { Config as ConfigV2 } from "../config"
import type { EventV2 } from "../event"
import { EventV2 as EventV2Service } from "../event"
import { Config } from "../config"
import { EventV2 } from "../event"
import { makeLocationNode } from "../effect/app-node"
import { llmClient } from "../effect/app-node-platform"
import { SessionEvent } from "./event"
@ -311,9 +309,9 @@ const make = (dependencies: Dependencies) => {
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Service.Service
const events = yield* EventV2.Service
const llm = yield* LLMClient.Service
const config = yield* ConfigV2.Service
const config = yield* Config.Service
const models = yield* SessionRunnerModel.Service
const compaction = make({ events, llm, config: yield* config.entries() })
@ -336,5 +334,5 @@ export const layer = Layer.effect(
export const node = makeLocationNode({
service: Service,
layer,
deps: [EventV2Service.node, llmClient, ConfigV2.node, SessionRunnerModel.node],
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node],
})

View file

@ -112,7 +112,7 @@ const rewrite = Effect.fnUntraced(function* (
.returning({ sessionID: SessionContextCheckpointTable.session_id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die("Context checkpoint not found")
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
})
const advance = Effect.fnUntraced(function* (
@ -127,5 +127,5 @@ const advance = Effect.fnUntraced(function* (
.returning({ sessionID: SessionContextCheckpointTable.session_id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die("Context checkpoint not found")
if (!updated) return yield* Effect.die(new Error("Context checkpoint not found"))
})

View file

@ -19,7 +19,7 @@ const layer = Layer.effect(
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError>({
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(`Session not found: ${sessionID}`)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return yield* SessionRunner.Service.use((runner) => runner.run({ sessionID, force })).pipe(
Effect.provide(locations.get(session.location)),
Effect.tapCause((cause) =>

View file

@ -62,7 +62,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* (
.pipe(
Effect.flatMap((event) =>
event.durable === undefined
? Effect.die("Prompt admission event is missing aggregate sequence")
? Effect.die(new Error("Prompt admission event is missing aggregate sequence"))
: Effect.succeed(
Admitted.make({
admittedSeq: event.durable.seq,

View file

@ -157,7 +157,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.where(eq(SessionTable.id, event.data.parentID))
.get()
.pipe(Effect.orDie)
if (!parent) return yield* Effect.die(`Fork parent session not found: ${event.data.parentID}`)
if (!parent) return yield* Effect.die(new Error(`Fork parent session not found: ${event.data.parentID}`))
const boundary = event.data.messageID
? yield* db
.select({ seq: SessionMessageTable.seq })
@ -172,7 +172,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
.pipe(Effect.orDie)
: undefined
if (event.data.messageID && !boundary)
return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`)
return yield* Effect.die(new Error(`Fork boundary message not found: ${event.data.messageID}`))
const copied = yield* db
.select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable)
@ -341,7 +341,8 @@ function run(db: DatabaseService, event: MessageEvent) {
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
decodeMessage({ ...row.data, id: row.id, type: row.type })
const updateMessage = (message: SessionMessage.Message) => {
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
if (event.durable === undefined)
return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
const encoded = encodeMessage(message)
const { id, type, ...data } = encoded
return db
@ -418,7 +419,7 @@ function run(db: DatabaseService, event: MessageEvent) {
}
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) {
if (event.durable === undefined) return Effect.die("Durable Session event is missing aggregate sequence")
if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
const encoded = encodeMessage(message)
const { id, type, ...data } = encoded
return db
@ -585,7 +586,8 @@ const layer = Layer.effectDiscard(
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
yield* events.project(SessionEvent.Prompted, (event) =>
Effect.gen(function* () {
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionInput.projectPrompted(db, {
id: event.data.messageID,
sessionID: event.data.sessionID,
@ -599,7 +601,8 @@ const layer = Layer.effectDiscard(
)
yield* events.project(SessionEvent.PromptAdmitted, (event) =>
Effect.gen(function* () {
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionInput.projectAdmitted(db, {
admittedSeq: event.durable.seq,
id: event.data.messageID,
@ -670,7 +673,7 @@ const layer = Layer.effectDiscard(
)
.get()
.pipe(Effect.orDie)
if (!boundary) return yield* Effect.die(`Revert boundary message not found: ${event.data.messageID}`)
if (!boundary) return yield* Effect.die(new Error(`Revert boundary message not found: ${event.data.messageID}`))
yield* db
.delete(SessionMessageTable)
.where(

View file

@ -1,3 +1,5 @@
export * as SessionRunnerLLM from "./llm"
import {
LLM,
LLMClient,
@ -119,7 +121,7 @@ const layer = Layer.effect(
const forkTitle = yield* FiberSet.makeRuntime<never, void, never>()
const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(`Session not found: ${sessionID}`)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return session
})

View file

@ -2,8 +2,11 @@ export * as SessionRunnerModel from "./model"
import { makeLocationNode } from "../../effect/app-node"
import { type Model } from "@opencode-ai/llm"
// ast-grep-ignore: no-star-import
import * as AnthropicMessages from "@opencode-ai/llm/protocols/anthropic-messages"
// ast-grep-ignore: no-star-import
import * as OpenAICompatibleChat from "@opencode-ai/llm/protocols/openai-compatible-chat"
// ast-grep-ignore: no-star-import
import * as OpenAIResponses from "@opencode-ai/llm/protocols/openai-responses"
import { Auth, type AnyRoute } from "@opencode-ai/llm/route"
import { Context, Effect, Layer, Schema } from "effect"

View file

@ -85,7 +85,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
})
const currentAssistantMessageID = () =>
assistantMessageID === undefined
? Effect.die("Tool event before assistant step start")
? Effect.die(new Error("Tool event before assistant step start"))
: Effect.succeed(assistantMessageID)
const fragments = (
@ -95,20 +95,20 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const chunks = new Map<string, string[]>()
const start = (id: string) =>
Effect.suspend(() => {
if (chunks.has(id)) return Effect.die(`Duplicate ${name} start: ${id}`)
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
chunks.set(id, [])
return Effect.void
})
const append = (id: string, value: string) =>
Effect.suspend(() => {
const current = chunks.get(id)
if (!current) return Effect.die(`${name} delta before start: ${id}`)
if (!current) return Effect.die(new Error(`${name} delta before start: ${id}`))
current.push(value)
return Effect.void
})
const end = Effect.fnUntraced(function* (id: string, providerMetadata?: ProviderMetadata) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(`${name} end before start: ${id}`)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* ended(id, current.join(""), providerMetadata)
chunks.delete(id)
})
@ -144,7 +144,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const toolInput = fragments("tool input", (callID, value) =>
Effect.gen(function* () {
const tool = tools.get(callID)
if (!tool) return yield* Effect.die(`Tool input end before start: ${callID}`)
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`))
yield* events.publish(SessionEvent.Tool.Input.Ended, {
sessionID: input.sessionID,
timestamp: yield* timestamp,
@ -163,7 +163,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
})
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
if (tools.has(event.id)) return yield* Effect.die(new Error(`Duplicate tool input start: ${event.id}`))
const assistantMessageID = yield* startAssistant()
tools.set(event.id, {
assistantMessageID,
@ -185,10 +185,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
const tool = tools.get(event.id)
if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`)
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${event.id}`))
if (tool.name !== event.name)
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.inputEnded) return yield* Effect.die(new Error(`Duplicate tool input end: ${event.id}`))
yield* toolInput.end(event.id)
})
@ -233,7 +233,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const assistantMessageIDForTool = (callID: string) => {
const tool = tools.get(callID)
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(`Unknown tool call: ${callID}`)
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`))
}
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
@ -293,10 +293,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return
case "tool-input-delta": {
const tool = tools.get(event.id)
if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`)
if (!tool) return yield* Effect.die(new Error(`Tool input delta before start: ${event.id}`))
if (tool.name !== event.name)
return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.inputEnded) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
yield* toolInput.append(event.id, event.text)
yield* events.publish(SessionEvent.Tool.Input.Delta, {
sessionID: input.sessionID,
@ -315,8 +315,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const tool = tools.get(event.id)!
if (!tool.inputEnded) yield* endToolInput(event)
if (tool.name !== event.name)
return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
return yield* Effect.die(new Error(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
tool.called = true
tool.providerExecuted = event.providerExecuted === true
tool.providerMetadata = event.providerMetadata
@ -336,12 +336,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
}
case "tool-result": {
const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`)
if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`))
if (tool.name !== event.name)
return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
return yield* Effect.die(new Error(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.settled) {
if (event.result.type === "error") return
return yield* Effect.die(`Duplicate tool result: ${event.id}`)
return yield* Effect.die(new Error(`Duplicate tool result: ${event.id}`))
}
tool.settled = true
const result = settledOutput(event.output, event.result)
@ -375,10 +375,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
}
case "tool-error": {
const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`)
if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`))
if (tool.name !== event.name)
return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`)
return yield* Effect.die(new Error(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`))
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`))
tool.settled = true
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: input.sessionID,
@ -396,7 +396,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
case "step-finish":
yield* flush()
assistantActive = false
if (stepSettlement) return yield* Effect.die("Duplicate step finish")
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
return
case "finish":

View file

@ -1,5 +1,5 @@
import { sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from "drizzle-orm/sqlite-core"
import * as DatabasePath from "../database/path"
import { directoryColumn, pathColumn } from "../database/path"
import { ProjectTable } from "../project/sql"
import type { SessionMessage } from "./message"
import type { Prompt } from "./prompt"
@ -31,8 +31,8 @@ export const SessionTable = sqliteTable(
workspace_id: text().$type<WorkspaceV2.ID>(),
parent_id: text().$type<SessionSchema.ID>(),
slug: text().notNull(),
directory: DatabasePath.directoryColumn().notNull(),
path: DatabasePath.pathColumn(),
directory: directoryColumn().notNull(),
path: pathColumn(),
title: text().notNull(),
version: text().notNull(),
share_url: text(),