feat(kilocode): introduce EffectBridge for async-to-effect interop and lazify runtime constructors

Replace raw `Effect.promise` calls across HTTP API handlers with
`EffectBridge.fromPromise` / `EffectBridge.make` to ensure proper error
propagation and context forwarding when bridging imperative async code
into the Effect runtime.

Wrap top-level `makeRuntime` calls in `plan-followup.ts` with `lazy()`
to defer service instantiation until first access, avoiding eager
initialization side-effects during module loading.

Also reorder SDK event union members and type definitions to reflect
updated OpenAPI schema generation output.
This commit is contained in:
Imanol Maiztegui
2026-05-19 14:33:15 +02:00
parent ad00b1461d
commit ecc2f7af19
10 changed files with 93 additions and 78 deletions
+12 -11
View File
@@ -19,39 +19,40 @@ import { Todo } from "@/session/todo"
import { makeRuntime } from "@/effect/run-service"
import * as Log from "@opencode-ai/core/util/log"
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
import { lazy } from "@/util/lazy"
import path from "path"
import z from "zod"
const agents = makeRuntime(Agent.Service, Agent.defaultLayer)
const providers = makeRuntime(Provider.Service, Provider.defaultLayer)
const questions = makeRuntime(Question.Service, Question.defaultLayer)
const todo = makeRuntime(Todo.Service, Todo.defaultLayer)
const agents = lazy(() => makeRuntime(Agent.Service, Agent.defaultLayer))
const providers = lazy(() => makeRuntime(Provider.Service, Provider.defaultLayer))
const questions = lazy(() => makeRuntime(Question.Service, Question.defaultLayer))
const todo = lazy(() => makeRuntime(Todo.Service, Todo.defaultLayer))
const pending = new Map<SessionID, AbortController>()
export const PlanFollowupRuntime = {
agent(name: string): Promise<Agent.Info | undefined> {
return agents.runPromise((svc) => svc.get(name))
return agents().runPromise((svc) => svc.get(name))
},
model(providerID: ProviderID, modelID: ModelID): Promise<Provider.Model> {
return providers.runPromise((svc) => svc.getModel(providerID, modelID))
return providers().runPromise((svc) => svc.getModel(providerID, modelID))
},
question: {
ask(input: Parameters<Question.Interface["ask"]>[0]) {
return questions.runPromise((svc) => svc.ask(input))
return questions().runPromise((svc) => svc.ask(input))
},
list() {
return questions.runPromise((svc) => svc.list())
return questions().runPromise((svc) => svc.list())
},
reject(requestID: Parameters<Question.Interface["reject"]>[0]) {
return questions.runPromise((svc) => svc.reject(requestID))
return questions().runPromise((svc) => svc.reject(requestID))
},
},
todo: {
get(sessionID: SessionID) {
return todo.runPromise((svc) => svc.get(sessionID))
return todo().runPromise((svc) => svc.get(sessionID))
},
update(input: Parameters<Todo.Interface["update"]>[0]) {
return todo.runPromise((svc) => svc.update(input))
return todo().runPromise((svc) => svc.update(input))
},
},
async loop(sessionID: SessionID) {
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { EffectBridge } from "@/effect/bridge"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
import { Config } from "@/config/config"
import { generateCommitMessage } from "@/kilocode/commit-message"
@@ -14,7 +15,7 @@ export const commitMessageHandlers = HttpApiBuilder.group(InstanceHttpApi, "comm
}) {
const cfg = yield* config.get()
const prompt = cfg.commit_message?.prompt || undefined
const result = yield* Effect.promise(() =>
const result = yield* EffectBridge.fromPromise(() =>
generateCommitMessage({
path: ctx.payload.path,
selectedFiles: ctx.payload.selectedFiles ? [...ctx.payload.selectedFiles] : undefined,
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { EffectBridge } from "@/effect/bridge"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
import { enhancePrompt } from "@/kilocode/enhance-prompt"
import { EnhancePromptPayload } from "../groups/enhance-prompt"
@@ -9,7 +10,7 @@ export const enhancePromptHandlers = HttpApiBuilder.group(InstanceHttpApi, "enha
const enhance = Effect.fn("EnhancePromptHttpApi.enhance")(function* (ctx: {
payload: typeof EnhancePromptPayload.Type
}) {
const text = yield* Effect.promise(() => enhancePrompt(ctx.payload.text))
const text = yield* EffectBridge.fromPromise(() => enhancePrompt(ctx.payload.text))
return { text }
})
@@ -1,12 +1,13 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { EffectBridge } from "@/effect/bridge"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
export const indexingHandlers = HttpApiBuilder.group(InstanceHttpApi, "indexing", (handlers) =>
Effect.gen(function* () {
const status = Effect.fn("IndexingHttpApi.status")(function* () {
const mod = yield* Effect.promise(() => import("@/kilocode/indexing"))
const current = yield* Effect.promise(() => mod.KiloIndexing.current())
const current = yield* EffectBridge.fromPromise(() => mod.KiloIndexing.current())
return current
})
@@ -25,6 +25,7 @@ import * as Stream from "effect/Stream"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { Auth } from "@/auth"
import { EffectBridge } from "@/effect/bridge"
import { Bus } from "@/bus"
import { Identifier } from "@/id/id"
import { Instance } from "@/project/instance"
@@ -279,19 +280,24 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo",
if (!fetched.ok) return yield* Effect.fail(new HttpApiError.BadRequest({}))
if (!fetched.data?.info?.id) return yield* Effect.fail(new HttpApiError.BadRequest({}))
return yield* Effect.try({
const bridge = yield* EffectBridge.make()
return yield* Effect.tryPromise({
try: () =>
importSessionToDb(fetched.data, {
Database,
Instance,
SessionTable,
MessageTable,
PartTable,
SessionToRow: Session.toRow,
Bus,
SessionCreatedEvent: Session.Event.Created,
Identifier,
}),
bridge.promise(
Effect.sync(() =>
importSessionToDb(fetched.data, {
Database,
Instance,
SessionTable,
MessageTable,
PartTable,
SessionToRow: Session.toRow,
Bus,
SessionCreatedEvent: Session.Event.Created,
Identifier,
}),
),
),
catch: () => new HttpApiError.BadRequest({}),
})
})
@@ -1,6 +1,7 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Agent } from "@/agent/agent"
import { EffectBridge } from "@/effect/bridge"
import { HeapSnapshot } from "@/kilocode/cli/heap-snapshot"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
import { Skill } from "@/skill"
@@ -22,7 +23,7 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode"
const removeAgent = Effect.fn("KilocodeHttpApi.removeAgent")(function* (ctx: {
payload: typeof RemoveAgentPayload.Type
}) {
yield* Effect.promise(() => Agent.remove(ctx.payload.name))
yield* EffectBridge.fromPromise(() => Agent.remove(ctx.payload.name))
return true
})
@@ -1,5 +1,6 @@
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { EffectBridge } from "@/effect/bridge"
import { QuestionID } from "@/question/schema"
import { SessionNetwork } from "@/session/network"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
@@ -7,16 +8,16 @@ import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
export const networkHandlers = HttpApiBuilder.group(InstanceHttpApi, "network", (handlers) =>
Effect.gen(function* () {
const list = Effect.fn("NetworkHttpApi.list")(function* () {
return yield* Effect.promise(() => SessionNetwork.list())
return yield* EffectBridge.fromPromise(() => SessionNetwork.list())
})
const reply = Effect.fn("NetworkHttpApi.reply")(function* (ctx: { params: { requestID: QuestionID } }) {
yield* Effect.promise(() => SessionNetwork.reply({ requestID: ctx.params.requestID }))
yield* EffectBridge.fromPromise(() => SessionNetwork.reply({ requestID: ctx.params.requestID }))
return true
})
const reject = Effect.fn("NetworkHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
yield* Effect.promise(() => SessionNetwork.reject({ requestID: ctx.params.requestID }))
yield* EffectBridge.fromPromise(() => SessionNetwork.reject({ requestID: ctx.params.requestID }))
return true
})
@@ -1,13 +1,15 @@
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { EffectBridge } from "@/effect/bridge"
import { KiloSessions } from "@/kilo-sessions/kilo-sessions"
import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api"
export const remoteHandlers = HttpApiBuilder.group(InstanceHttpApi, "remote", (handlers) =>
Effect.gen(function* () {
const enable = Effect.fn("RemoteHttpApi.enable")(function* () {
const bridge = yield* EffectBridge.make()
yield* Effect.tryPromise({
try: () => KiloSessions.enableRemote(),
try: () => bridge.promise(Effect.promise(() => KiloSessions.enableRemote())),
catch: () => new HttpApiError.Unauthorized(),
})
return KiloSessions.remoteStatus()
@@ -1,6 +1,7 @@
import { Account } from "@/account/account"
import { Agent } from "@/agent/agent"
import { Config } from "@/config/config"
import { EffectBridge } from "@/effect/bridge" // kilocode_change
import { InstanceState } from "@/effect/instance-state"
import { MCP } from "@/mcp"
import { Project } from "@/project/project"
@@ -128,7 +129,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
// kilocode_change start - worktree diff endpoints for agent manager
const base = Effect.fn("ExperimentalHttpApi.worktreeDiffBase")(function* (input: { base?: string }) {
if (input.base) return input.base
return yield* Effect.promise(() => Review.getBaseBranch())
return yield* EffectBridge.fromPromise(() => Review.getBaseBranch())
})
const worktreeDiff = Effect.fn("ExperimentalHttpApi.worktreeDiff")(function* (ctx: {
@@ -178,7 +179,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
// kilocode_change start
const state = yield* InstanceState.context
const projectID = ctx.query.worktrees && !ctx.query.projectID ? state.project.id : ctx.query.projectID
const directories = ctx.query.worktrees ? yield* Effect.promise(() => WorktreeFamily.list()) : undefined
const directories = ctx.query.worktrees ? yield* EffectBridge.fromPromise(() => WorktreeFamily.list()) : undefined
const sorted = directories ? [...directories].sort((a, b) => b.length - a.length) : undefined
// kilocode_change end
const sessions = Array.from(
+45 -45
View File
@@ -35,8 +35,6 @@ export type Event =
| EventInstallationUpdateAvailable
| EventFileEdited
| EventFileWatcherUpdated
| EventCommandExecuted
| EventProjectUpdated
| EventTodoUpdated
| EventSessionStatus
| EventSessionIdle
@@ -44,6 +42,8 @@ export type Event =
| EventSuggestionAccepted
| EventSuggestionDismissed
| EventSessionCompacted
| EventCommandExecuted
| EventProjectUpdated
| EventKilocodeAgentManagerStart
| EventVcsBranchUpdated
| EventWorkspaceReady
@@ -328,30 +328,6 @@ export type ApiError = {
}
}
export type Project = {
id: string
worktree: string
vcs?: "git"
name?: string
icon?: {
url?: string
override?: string
color?: string
}
commands?: {
/**
* Startup script to run when creating a new workspace (worktree)
*/
start?: string
}
time: {
created: number
updated: number
initialized?: number
}
sandboxes: Array<string>
}
export type Todo = {
/**
* Brief description of the task
@@ -408,6 +384,30 @@ export type SuggestionRequest = {
}
}
export type Project = {
id: string
worktree: string
vcs?: "git"
name?: string
icon?: {
url?: string
override?: string
color?: string
}
commands?: {
/**
* Startup script to run when creating a new workspace (worktree)
*/
start?: string
}
time: {
created: number
updated: number
initialized?: number
}
sandboxes: Array<string>
}
export type Pty = {
id: string
title: string
@@ -880,8 +880,6 @@ export type GlobalEvent = {
| EventInstallationUpdateAvailable
| EventFileEdited
| EventFileWatcherUpdated
| EventCommandExecuted
| EventProjectUpdated
| EventTodoUpdated
| EventSessionStatus
| EventSessionIdle
@@ -889,6 +887,8 @@ export type GlobalEvent = {
| EventSuggestionAccepted
| EventSuggestionDismissed
| EventSessionCompacted
| EventCommandExecuted
| EventProjectUpdated
| EventKilocodeAgentManagerStart
| EventVcsBranchUpdated
| EventWorkspaceReady
@@ -2687,23 +2687,6 @@ export type EventFileWatcherUpdated = {
}
}
export type EventCommandExecuted = {
id: string
type: "command.executed"
properties: {
name: string
sessionID: string
arguments: string
messageID: string
}
}
export type EventProjectUpdated = {
id: string
type: "project.updated"
properties: Project
}
export type EventTodoUpdated = {
id: string
type: "todo.updated"
@@ -2774,6 +2757,23 @@ export type EventSessionCompacted = {
}
}
export type EventCommandExecuted = {
id: string
type: "command.executed"
properties: {
name: string
sessionID: string
arguments: string
messageID: string
}
}
export type EventProjectUpdated = {
id: string
type: "project.updated"
properties: Project
}
export type EventKilocodeAgentManagerStart = {
id: string
type: "kilocode.agent_manager.start"