opencode/packages/core/src/session/runtime.ts
2026-06-28 19:47:17 +00:00

165 lines
6.9 KiB
TypeScript

export * as SessionRuntime from "./runtime"
import { Context, Effect, Layer } from "effect"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { Location } from "../location"
import { PromptInput } from "@opencode-ai/schema/prompt-input"
import { SessionMessage } from "./message"
import { Prompt } from "./prompt"
import { SessionInput } from "./input"
import { SessionRevert } from "./revert"
import { SessionRunner } from "./runner"
import * as SessionRunnerLLM from "./runner/llm"
import { SessionRuntimeCoordinator } from "./runtime-coordinator"
import { SessionSchema } from "./schema"
import { Snapshot } from "../snapshot"
import { FSUtil } from "../fs-util"
import { makeLocationNode } from "../effect/app-node"
import { SessionV2 } from "../session"
import {
BusyError,
MessageNotFoundError,
NotFoundError,
PromptConflictError,
type RevertState,
} from "../session"
export interface Interface {
readonly prompt: (input: {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
prompt: PromptInput.Prompt
delivery?: SessionInput.Delivery
resume?: boolean
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError>
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
readonly revert: {
readonly stage: (input: {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
files?: boolean
}) => Effect.Effect<RevertState, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
}
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRuntime") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const database = yield* Database.Service
const db = database.db
const events = yield* EventV2.Service
const location = yield* Location.Service
const sessions = yield* SessionV2.Service
const runner = yield* SessionRunner.Service
const snapshot = yield* Snapshot.Service
const coordinator = yield* SessionRuntimeCoordinator.Service
const local = Effect.fn("SessionRuntime.local")(function* (sessionID: SessionSchema.ID) {
const session = yield* sessions.get(sessionID)
if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID)
return yield* new NotFoundError({ sessionID })
return session
})
const drain = (sessionID: SessionSchema.ID) =>
Effect.fnUntraced(function* (force: boolean) {
yield* local(sessionID).pipe(Effect.orDie)
return yield* runner.run({ sessionID, force })
})
return Service.of({
prompt: Effect.fn("SessionRuntime.prompt")((input) =>
Effect.uninterruptible(
Effect.gen(function* () {
yield* local(input.sessionID)
const prompt = resolvePrompt(input.prompt)
const messageID = input.id ?? SessionMessage.ID.create()
const delivery = input.delivery ?? "steer"
const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
const admitted = yield* SessionInput.admit(db, events, {
id: messageID,
sessionID: input.sessionID,
prompt,
delivery,
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionInput.LifecycleConflict
? new PromptConflictError({ sessionID: input.sessionID, messageID })
: Effect.die(defect),
),
)
if (!SessionInput.equivalent(admitted, expected))
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
if (input.resume !== false) yield* coordinator.wake(admitted.sessionID, drain(admitted.sessionID))
return admitted
}),
),
),
wait: Effect.fn("SessionRuntime.wait")(function* (sessionID) {
yield* local(sessionID)
yield* coordinator.wait(sessionID)
}),
active: coordinator.active,
resume: Effect.fn("SessionRuntime.resume")(function* (sessionID) {
yield* local(sessionID)
yield* coordinator.run(sessionID, drain(sessionID))
}),
interrupt: Effect.fn("SessionRuntime.interrupt")(function* (sessionID) {
yield* local(sessionID)
yield* Effect.uninterruptible(coordinator.interrupt(sessionID))
}),
revert: {
stage: Effect.fn("SessionRuntime.revert.stage")(function* (input) {
const session = yield* local(input.sessionID)
if ((yield* coordinator.active).has(input.sessionID)) return yield* new BusyError({ sessionID: input.sessionID })
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
Effect.provideService(Database.Service, database),
Effect.provideService(EventV2.Service, events),
Effect.provideService(Snapshot.Service, snapshot),
)
}),
clear: Effect.fn("SessionRuntime.revert.clear")(function* (sessionID) {
const session = yield* local(sessionID)
if ((yield* coordinator.active).has(sessionID)) return yield* new BusyError({ sessionID })
return yield* SessionRevert.clear(session).pipe(
Effect.provideService(EventV2.Service, events),
Effect.provideService(Snapshot.Service, snapshot),
)
}),
commit: Effect.fn("SessionRuntime.revert.commit")(function* (sessionID) {
const session = yield* local(sessionID)
if ((yield* coordinator.active).has(sessionID)) return yield* new BusyError({ sessionID })
return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
}),
},
})
}),
)
const resolvePrompt = (input: PromptInput.Prompt) =>
Prompt.make({
text: input.text,
agents: input.agents,
files: input.files?.map((file) => {
const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1]
const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri)
return {
...file,
mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)),
}
}),
})
export const node = makeLocationNode({
service: Service,
layer,
deps: [Database.node, EventV2.node, Location.node, SessionV2.node, SessionRunnerLLM.node, Snapshot.node, SessionRuntimeCoordinator.node],
})