From 24d1f693efd14359753004c85d36257cf2bbf4bc Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Mon, 27 Apr 2026 18:34:48 +0200 Subject: [PATCH] refactor(opencode): migrate zod schemas to effect Schema across kilo modules Replace zod schema definitions with effect Schema equivalents in BusEvent definitions, tool parameters, session network, suggestions, and related modules. This aligns Kilo-specific code with the upstream v1.14.23 migration from zod to effect Schema. - Convert BusEvent property schemas from zod objects to Schema.Struct - Migrate tool parameter definitions (recall, warpgrep, suggest) to Schema.Struct with annotated descriptions - Replace zod-based SessionNetwork.Wait with effect Schema and add zod interop via withStatics helper - Extract shared EditorContext schema in message-v2 and reuse in prompt input - Replace `fn()` wrappers in session.ts with explicit Schema.decodeUnknownSync calls for type safety - Use branded SessionID/QuestionID types in place of raw z.string() - Update pending map in SessionNetwork from Record to Map - Update tests to use SessionID.make() for branded identifiers --- .../src/kilo-sessions/kilo-sessions.ts | 7 +- .../src/kilo-sessions/remote-sender.ts | 7 +- .../opencode/src/kilocode/session/index.ts | 15 +-- .../opencode/src/kilocode/suggestion/index.ts | 54 +++++++--- .../opencode/src/kilocode/suggestion/tool.ts | 13 +-- .../src/server/routes/instance/network.ts | 7 +- packages/opencode/src/session/message-v2.ts | 19 ++-- packages/opencode/src/session/network.ts | 102 +++++++++--------- packages/opencode/src/session/prompt.ts | 16 +-- packages/opencode/src/session/session.ts | 41 +++++-- packages/opencode/src/session/status.ts | 3 +- packages/opencode/src/tool/recall.ts | 23 ++-- packages/opencode/src/tool/warpgrep.ts | 13 ++- .../opencode/test/session/network.test.ts | 9 +- 14 files changed, 196 insertions(+), 133 deletions(-) diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 59cf4a90bb4..32719a1c92a 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -14,6 +14,7 @@ import { IngestQueue } from "@/kilo-sessions/ingest-queue" import { clearInFlightCache, withInFlightCache } from "@/kilo-sessions/inflight-cache" import type * as SDK from "@kilocode/sdk/v2" import z from "zod" +import { Schema } from "effect" import { KILO_API_BASE } from "@kilocode/kilo-gateway" import { Config } from "@/config" import { Instance } from "@/project/instance" @@ -31,9 +32,9 @@ export namespace KiloSessions { export const Event = { RemoteStatusChanged: BusEvent.define( "kilo-sessions.remote-status-changed", - z.object({ - enabled: z.boolean(), - connected: z.boolean(), + Schema.Struct({ + enabled: Schema.Boolean, + connected: Schema.Boolean, }), ), } diff --git a/packages/opencode/src/kilo-sessions/remote-sender.ts b/packages/opencode/src/kilo-sessions/remote-sender.ts index 40feff1afe5..0148d859573 100644 --- a/packages/opencode/src/kilo-sessions/remote-sender.ts +++ b/packages/opencode/src/kilo-sessions/remote-sender.ts @@ -17,6 +17,7 @@ import { QuestionID } from "@/question/schema" import { ModelID, ProviderID } from "@/provider/schema" import { Log } from "@/util" import z from "zod" +import { zodObject } from "@/util/effect-zod" const QuestionData = z.object({ requestID: z.string(), @@ -36,9 +37,9 @@ const SuggestionData = z.object({ // kilocode_change start — lazy init to avoid circular dependency // (Server → RemoteRoutes → RemoteSender → SessionPrompt at module load time) -let _remotePromptInput: ReturnType | undefined +let _remotePromptInput: z.ZodObject | undefined function getRemotePromptInput() { - return (_remotePromptInput ??= SessionPrompt.PromptInput.extend({ + return (_remotePromptInput ??= zodObject(SessionPrompt.PromptInput).extend({ model: z.string().optional(), })) } @@ -265,7 +266,7 @@ export namespace RemoteSender { }) return } - const input = SessionPrompt.PromptInput.safeParse( + const input = SessionPrompt.PromptInput.zod.safeParse( normalizePrompt(parsed.data as SessionPrompt.PromptInput & { model?: string }), ) if (!input.success) { diff --git a/packages/opencode/src/kilocode/session/index.ts b/packages/opencode/src/kilocode/session/index.ts index 152aee872c5..47019233897 100644 --- a/packages/opencode/src/kilocode/session/index.ts +++ b/packages/opencode/src/kilocode/session/index.ts @@ -1,6 +1,7 @@ // kilocode_change - new file import { remapChildren as _remapChildren } from "./fork" import z from "zod" +import { Schema } from "effect" import { BusEvent } from "@/bus/bus-event" import { Session } from "@/session" import { MessageID, SessionID } from "@/session/schema" @@ -23,23 +24,25 @@ export namespace KiloSession { // Events // --------------------------------------------------------------------------- + const CloseReasonSchema = Schema.Literals(["completed", "error", "interrupted"]) + export const Event = { TurnOpen: BusEvent.define( "session.turn.open", - z.object({ - sessionID: z.string(), + Schema.Struct({ + sessionID: SessionID, }), ), TurnClose: BusEvent.define( "session.turn.close", - z.object({ - sessionID: z.string(), - reason: z.enum(["completed", "error", "interrupted"]), + Schema.Struct({ + sessionID: SessionID, + reason: CloseReasonSchema, }), ), } - export type CloseReason = z.infer["reason"] + export type CloseReason = Schema.Schema.Type // --------------------------------------------------------------------------- // Per-session platform override (telemetry attribution) diff --git a/packages/opencode/src/kilocode/suggestion/index.ts b/packages/opencode/src/kilocode/suggestion/index.ts index 4607d09602c..ac34b618fb1 100644 --- a/packages/opencode/src/kilocode/suggestion/index.ts +++ b/packages/opencode/src/kilocode/suggestion/index.ts @@ -2,8 +2,10 @@ import { Bus } from "../../bus" import { BusEvent } from "../../bus/bus-event" import { Identifier } from "../../id/id" import { SessionID } from "../../session/schema" +import { ZodOverride } from "../../util/effect-zod" import { Log } from "../../util" import z from "zod" +import { Schema } from "effect" import { KiloSessionPromptQueue } from "../session/prompt-queue" export namespace Suggestion { @@ -20,6 +22,18 @@ export namespace Suggestion { }) export type Action = z.infer + export const ActionSchema = Schema.Struct({ + label: Schema.String.annotate({ description: "Button or option label (1-5 words)" }), + description: Schema.optional(Schema.String).annotate({ + description: "Brief explanation of what this action does", + }), + prompt: Schema.String.annotate({ + description: "Synthetic user prompt to inject when this action is accepted", + }), + }) + + const SuggestionIDSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("suggestion") }) + export const Info = z .object({ text: z.string().describe("Suggestion text shown to the user"), @@ -54,27 +68,41 @@ export namespace Suggestion { }) export type Request = z.infer + const RequestSchema = Schema.Struct({ + id: SuggestionIDSchema, + sessionID: SessionID, + text: Schema.String, + actions: Schema.Array(ActionSchema).check(Schema.isMinLength(1), Schema.isMaxLength(2)), + blocking: Schema.optional(Schema.Boolean), + tool: Schema.optional( + Schema.Struct({ + messageID: Schema.String, + callID: Schema.String, + }), + ), + }) + export const Accept = z.object({ index: z.number().int().nonnegative().describe("Zero-based action index to accept"), }) export type Accept = z.infer export const Event = { - Shown: BusEvent.define("suggestion.shown", Request), + Shown: BusEvent.define("suggestion.shown", RequestSchema), Accepted: BusEvent.define( "suggestion.accepted", - z.object({ - sessionID: z.string(), - requestID: z.string(), - index: z.number().int().nonnegative(), - action: Action, + Schema.Struct({ + sessionID: SessionID, + requestID: SuggestionIDSchema, + index: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), + action: ActionSchema, }), ), Dismissed: BusEvent.define( "suggestion.dismissed", - z.object({ - sessionID: z.string(), - requestID: z.string(), + Schema.Struct({ + sessionID: SessionID, + requestID: SuggestionIDSchema, }), ), } @@ -124,7 +152,7 @@ export namespace Suggestion { resolve, reject, } - Bus.publish(Event.Shown, info) + Bus.publish(Event.Shown, { ...info, sessionID: SessionID.make(info.sessionID) }) }) } @@ -149,7 +177,7 @@ export namespace Suggestion { log.info("accepted", { requestID: input.requestID, index: input.index, label: action.label }) Bus.publish(Event.Accepted, { - sessionID: existing.info.sessionID, + sessionID: SessionID.make(existing.info.sessionID), requestID: existing.info.id, index: input.index, action, @@ -171,7 +199,7 @@ export namespace Suggestion { log.info("dismissed", { requestID }) Bus.publish(Event.Dismissed, { - sessionID: existing.info.sessionID, + sessionID: SessionID.make(existing.info.sessionID), requestID: existing.info.id, }) @@ -192,7 +220,7 @@ export namespace Suggestion { delete s.pending[id] log.info("dismissed", { requestID: id }) Bus.publish(Event.Dismissed, { - sessionID: entry.info.sessionID, + sessionID: SessionID.make(entry.info.sessionID), requestID: entry.info.id, }) entry.reject(new DismissedError()) diff --git a/packages/opencode/src/kilocode/suggestion/tool.ts b/packages/opencode/src/kilocode/suggestion/tool.ts index a0f59e6bd49..30e1cd78d93 100644 --- a/packages/opencode/src/kilocode/suggestion/tool.ts +++ b/packages/opencode/src/kilocode/suggestion/tool.ts @@ -1,7 +1,6 @@ import { Command } from "../../command" import { Log } from "../../util" -import z from "zod" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import DESCRIPTION from "./tool.txt" import { Tool } from "../../tool" import { Suggestion } from "./index" @@ -10,9 +9,11 @@ import { SessionID } from "../../session/schema" const log = Log.create({ service: "tool.suggest" }) -const Params = z.object({ - suggest: z.string().describe("Short suggestion text shown to the user"), - actions: z.array(Suggestion.Action).min(1).max(2).describe("Available actions the user can take"), +const Params = Schema.Struct({ + suggest: Schema.String.annotate({ description: "Short suggestion text shown to the user" }), + actions: Schema.Array(Suggestion.ActionSchema) + .check(Schema.isMinLength(1), Schema.isMaxLength(2)) + .annotate({ description: "Available actions the user can take" }), }) type Meta = { @@ -62,7 +63,7 @@ export const SuggestTool = Tool.define( const promise = Suggestion.show({ sessionID: ctx.sessionID, text: params.suggest, - actions: params.actions, + actions: params.actions.map((a) => ({ ...a })), blocking: false, // render above an active input; VS Code does the same tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, }) diff --git a/packages/opencode/src/server/routes/instance/network.ts b/packages/opencode/src/server/routes/instance/network.ts index d72303a0ff9..da4826e995e 100644 --- a/packages/opencode/src/server/routes/instance/network.ts +++ b/packages/opencode/src/server/routes/instance/network.ts @@ -5,6 +5,7 @@ import z from "zod" import { lazy } from "@/util/lazy" import { errors } from "../../error" import { SessionNetwork } from "@/session/network" +import { QuestionID } from "@/question/schema" export const NetworkRoutes = lazy(() => new Hono() @@ -19,7 +20,7 @@ export const NetworkRoutes = lazy(() => description: "List of pending network reconnect requests", content: { "application/json": { - schema: resolver(SessionNetwork.Wait.array()), + schema: resolver(SessionNetwork.Wait.zod.array()), }, }, }, @@ -51,7 +52,7 @@ export const NetworkRoutes = lazy(() => validator( "param", z.object({ - requestID: z.string(), + requestID: QuestionID.zod, }), ), async (c) => { @@ -81,7 +82,7 @@ export const NetworkRoutes = lazy(() => validator( "param", z.object({ - requestID: z.string(), + requestID: QuestionID.zod, }), ), async (c) => { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index acf80147474..1de74a39f0a 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -370,6 +370,16 @@ const messageBase = { sessionID: SessionID, } +// kilocode_change start - shared editor context schema (used by MessageV2.User and SessionPrompt.PromptInput) +export const EditorContext = Schema.Struct({ + visibleFiles: Schema.optional(Schema.Array(Schema.String)), + openTabs: Schema.optional(Schema.Array(Schema.String)), + activeFile: Schema.optional(Schema.String), + shell: Schema.optional(Schema.String), +}) +export type EditorContext = Types.DeepMutable> +// kilocode_change end + export const User = Schema.Struct({ ...messageBase, role: Schema.Literal("user"), @@ -393,14 +403,7 @@ export const User = Schema.Struct({ system: Schema.optional(Schema.String), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), // kilocode_change start - editorContext: Schema.optional( - Schema.Struct({ - visibleFiles: Schema.optional(Schema.Array(Schema.String)), - openTabs: Schema.optional(Schema.Array(Schema.String)), - activeFile: Schema.optional(Schema.String), - shell: Schema.optional(Schema.String), - }), - ), + editorContext: Schema.optional(EditorContext), // kilocode_change end }) .annotate({ identifier: "UserMessage" }) diff --git a/packages/opencode/src/session/network.ts b/packages/opencode/src/session/network.ts index f2816ea05f8..8074ff11220 100644 --- a/packages/opencode/src/session/network.ts +++ b/packages/opencode/src/session/network.ts @@ -1,13 +1,16 @@ // kilocode_change - new file -import { Context, Effect, Layer } from "effect" +import { Context, Effect, Layer, Schema, Types } from "effect" import { Bus } from "../bus" import { BusEvent } from "../bus/bus-event" -import { Identifier } from "../id/id" +import { QuestionID } from "../question/schema" +import { SessionID } from "../session/schema" import { InstanceState } from "@/effect" import { makeRuntime } from "@/effect/run-service" import { Log } from "../util" import { fn } from "../util/fn" import { MCP } from "../mcp" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import z from "zod" export namespace SessionNetwork { @@ -39,51 +42,49 @@ export namespace SessionNetwork { }) } - export const Wait = z - .object({ - id: Identifier.schema("question"), - sessionID: Identifier.schema("session"), - message: z.string(), - restored: z.boolean(), - time: z.object({ - created: z.number(), - }), - }) - .meta({ - ref: "SessionNetworkWait", - }) - export type Wait = z.infer + export const Wait = Schema.Struct({ + id: QuestionID, + sessionID: SessionID, + message: Schema.String, + restored: Schema.Boolean, + time: Schema.Struct({ + created: Schema.Number, + }), + }) + .annotate({ identifier: "SessionNetworkWait" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) + export type Wait = Schema.Schema.Type export const Event = { Asked: BusEvent.define("session.network.asked", Wait), Replied: BusEvent.define( "session.network.replied", - z.object({ - sessionID: z.string(), - requestID: z.string(), + Schema.Struct({ + sessionID: SessionID, + requestID: QuestionID, }), ), Rejected: BusEvent.define( "session.network.rejected", - z.object({ - sessionID: z.string(), - requestID: z.string(), + Schema.Struct({ + sessionID: SessionID, + requestID: QuestionID, }), ), Restored: BusEvent.define( "session.network.restored", - z.object({ - sessionID: z.string(), - requestID: z.string(), + Schema.Struct({ + sessionID: SessionID, + requestID: QuestionID, }), ), } interface StateShape { - pending: Record< - string, + pending: Map< + QuestionID, { - info: Wait + info: Types.Mutable resolve: () => void reject: (e: unknown) => void } @@ -99,7 +100,7 @@ export namespace SessionNetwork { Effect.gen(function* () { const is = yield* InstanceState.make( Effect.fn("SessionNetwork.state")(function* () { - return { pending: {} } as StateShape + return { pending: new Map() } as StateShape }), ) return StateService.of({ @@ -161,12 +162,12 @@ export namespace SessionNetwork { return info.length > 0 } - async function watch(input: { requestID: string; abort: AbortSignal }) { + async function watch(input: { requestID: QuestionID; abort: AbortSignal }) { while (!input.abort.aborted) { await Bun.sleep(POLL_MS) if (input.abort.aborted) return const s = await state() - const req = s.pending[input.requestID] + const req = s.pending.get(input.requestID) if (!req || req.info.restored) return const ok = await probe().catch(() => false) if (!ok) continue @@ -175,9 +176,9 @@ export namespace SessionNetwork { } } - export async function ask(input: { sessionID: string; message: string; abort: AbortSignal }) { + export async function ask(input: { sessionID: SessionID; message: string; abort: AbortSignal }) { const s = await state() - const id = Identifier.ascending("question") + const id = QuestionID.ascending() const info: Wait = { id, sessionID: input.sessionID, @@ -190,16 +191,16 @@ export namespace SessionNetwork { const promise = new Promise((resolve, reject) => { const onAbort = () => { - if (!s.pending[id]) return + if (!s.pending.has(id)) return input.abort.removeEventListener("abort", onAbort) - delete s.pending[id] + s.pending.delete(id) Bus.publish(Event.Rejected, { sessionID: input.sessionID, requestID: id, }) reject(new DOMException("Aborted", "AbortError")) } - s.pending[id] = { + s.pending.set(id, { info, resolve: () => { input.abort.removeEventListener("abort", onAbort) @@ -209,7 +210,7 @@ export namespace SessionNetwork { input.abort.removeEventListener("abort", onAbort) reject(err) }, - } + }) input.abort.addEventListener("abort", onAbort, { once: true }) if (input.abort.aborted) { onAbort() @@ -226,14 +227,15 @@ export namespace SessionNetwork { export const restore = fn( z.object({ - requestID: z.string(), + requestID: QuestionID.zod, }), async (input) => { const s = await state() - const req = s.pending[input.requestID] + const requestID = input.requestID as QuestionID + const req = s.pending.get(requestID) if (!req || req.info.restored) return req.info.restored = true - log.info("network restored", { sessionID: req.info.sessionID, requestID: input.requestID }) + log.info("network restored", { sessionID: req.info.sessionID, requestID }) Bus.publish(Event.Restored, { sessionID: req.info.sessionID, requestID: req.info.id, @@ -243,16 +245,17 @@ export namespace SessionNetwork { export const reply = fn( z.object({ - requestID: z.string(), + requestID: QuestionID.zod, }), async (input) => { const s = await state() - const req = s.pending[input.requestID] + const requestID = input.requestID as QuestionID + const req = s.pending.get(requestID) if (!req) { - log.warn("reply for unknown request", { requestID: input.requestID }) + log.warn("reply for unknown request", { requestID }) return } - delete s.pending[input.requestID] + s.pending.delete(requestID) // kilocode_change start — reconnect failed remote MCP servers after network recovery void MCP.status() .then((statuses) => { @@ -278,16 +281,17 @@ export namespace SessionNetwork { export const reject = fn( z.object({ - requestID: z.string(), + requestID: QuestionID.zod, }), async (input) => { const s = await state() - const req = s.pending[input.requestID] + const requestID = input.requestID as QuestionID + const req = s.pending.get(requestID) if (!req) { - log.warn("reject for unknown request", { requestID: input.requestID }) + log.warn("reject for unknown request", { requestID }) return } - delete s.pending[input.requestID] + s.pending.delete(requestID) Bus.publish(Event.Rejected, { sessionID: req.info.sessionID, requestID: req.info.id, @@ -297,7 +301,7 @@ export namespace SessionNetwork { ) export async function list() { - return state().then((s) => Object.values(s.pending).map((item) => item.info)) + return state().then((s) => Array.from(s.pending.values()).map((item) => item.info)) } export class RejectedError extends Error { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 40c0b6e3ca1..3fc8032269a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1858,15 +1858,8 @@ export const PromptInput = Schema.Struct({ format: Schema.optional(MessageV2.Format), system: Schema.optional(Schema.String), variant: Schema.optional(Schema.String), - // kilocode_change start - editorContext: Schema.optional( - Schema.Struct({ - visibleFiles: Schema.optional(Schema.Array(Schema.String)), - openTabs: Schema.optional(Schema.Array(Schema.String)), - activeFile: Schema.optional(Schema.String), - shell: Schema.optional(Schema.String), - }), - ), + // kilocode_change start - reuse shared editor context schema + editorContext: Schema.optional(MessageV2.EditorContext), // kilocode_change end parts: Schema.Array( Schema.Union([ @@ -1886,8 +1879,9 @@ type PartInputUnion = | MessageV2.FilePartInput | MessageV2.AgentPartInput | MessageV2.SubtaskPartInput -export type PromptInput = Omit, "parts"> & { +export type PromptInput = Omit, "parts" | "editorContext"> & { parts: PartInputUnion[] + editorContext?: MessageV2.EditorContext } export class LoopInput extends Schema.Class("SessionPrompt.LoopInput")({ @@ -1970,7 +1964,7 @@ const quoteTrimRegex = /^["']|["']$/g // kilocode_change start - legacy promise helpers for Kilo callsites const { runPromise } = makeRuntime(Service, defaultLayer) export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(input)) -export const loop = (input: z.infer) => runPromise((svc) => svc.loop(input)) +export const loop = (input: LoopInput) => runPromise((svc) => svc.loop(input)) export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID)) // kilocode_change end diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index e30d0f60400..3d44ae53b7d 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -861,18 +861,37 @@ export function* listGlobal(input?: { // kilocode_change start - keep legacy promise helpers for Kilo callsites const { runPromise } = makeRuntime(Service, defaultLayer) -export const create = fn(CreateInput, (input) => runPromise((svc) => svc.create(input))) +const decodeCreate = Schema.decodeUnknownSync(CreateInput) +const decodeGet = Schema.decodeUnknownSync(GetInput) +const decodeSetTitle = Schema.decodeUnknownSync(SetTitleInput) +const decodeSetArchived = Schema.decodeUnknownSync(SetArchivedInput) +const decodeSetPermission = Schema.decodeUnknownSync(SetPermissionInput) +const decodeSetRevert = Schema.decodeUnknownSync(SetRevertInput) +const decodeMessages = Schema.decodeUnknownSync(MessagesInput) +const decodeChildren = Schema.decodeUnknownSync(ChildrenInput) +const decodeRemove = Schema.decodeUnknownSync(RemoveInput) + +export const create = (input?: CreateInput) => runPromise((svc) => svc.create(decodeCreate(input) as CreateInput)) export const fork = kiloSessionFork -export const get = fn(GetInput, (id) => runPromise((svc) => svc.get(id))) -export const setTitle = fn(SetTitleInput, (input) => runPromise((svc) => svc.setTitle(input))) -export const setArchived = fn(SetArchivedInput, (input) => runPromise((svc) => svc.setArchived(input))) -export const setPermission = fn(SetPermissionInput, (input) => runPromise((svc) => svc.setPermission(input))) -export const setRevert = fn(SetRevertInput, (input) => - runPromise((svc) => svc.setRevert({ sessionID: input.sessionID, revert: input.revert, summary: input.summary })), -) -export const messages = fn(MessagesInput, (input) => runPromise((svc) => svc.messages(input))) -export const children = fn(ChildrenInput, (id) => runPromise((svc) => svc.children(id))) -export const remove = fn(RemoveInput, (id) => runPromise((svc) => svc.remove(id))) +export const get = (id: SessionID) => runPromise((svc) => svc.get(decodeGet(id))) +export const setTitle = (input: { sessionID: SessionID; title: string }) => + runPromise((svc) => svc.setTitle(decodeSetTitle(input))) +export const setArchived = (input: { sessionID: SessionID; time?: number }) => + runPromise((svc) => svc.setArchived(decodeSetArchived(input))) +export const setPermission = (input: { sessionID: SessionID; permission: Permission.Ruleset }) => + runPromise((svc) => + svc.setPermission(decodeSetPermission(input) as { sessionID: SessionID; permission: Permission.Ruleset }), + ) +export const setRevert = (input: { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] }) => { + const parsed = decodeSetRevert(input) as { sessionID: SessionID; revert?: Info["revert"]; summary?: Info["summary"] } + return runPromise((svc) => + svc.setRevert({ sessionID: parsed.sessionID, revert: parsed.revert, summary: parsed.summary }), + ) +} +export const messages = (input: { sessionID: SessionID; limit?: number }) => + runPromise((svc) => svc.messages(decodeMessages(input))) +export const children = (id: SessionID) => runPromise((svc) => svc.children(decodeChildren(id))) +export const remove = (id: SessionID) => runPromise((svc) => svc.remove(decodeRemove(id))) export async function updateMessage(msg: T): Promise { MessageV2.Info.zod.parse(msg) // kilocode_change return runPromise((svc) => svc.updateMessage(msg)) diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 826660a1202..6c1d4f3d81f 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -2,6 +2,7 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { InstanceState } from "@/effect" import { SessionID } from "./schema" +import { QuestionID } from "@/question/schema" // kilocode_change import { makeRuntime } from "@/effect/run-service" // kilocode_change import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" @@ -24,7 +25,7 @@ export const Info = Schema.Union([ // kilocode_change start Schema.Struct({ type: Schema.Literal("offline"), - requestID: Schema.String, + requestID: QuestionID, message: Schema.String, }), // kilocode_change end diff --git a/packages/opencode/src/tool/recall.ts b/packages/opencode/src/tool/recall.ts index d5e9338fdac..8681d5d4588 100644 --- a/packages/opencode/src/tool/recall.ts +++ b/packages/opencode/src/tool/recall.ts @@ -1,6 +1,5 @@ // kilocode_change - new file -import z from "zod" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import * as Tool from "./tool" import { Instance } from "../project/instance" import { Locale } from "../util" @@ -8,11 +7,19 @@ import { Filesystem } from "../util" // kilocode_change import { WorktreeFamily } from "../kilocode/worktree-family" // kilocode_change import DESCRIPTION from "./recall.txt" -const Parameters = z.object({ - mode: z.enum(["search", "read"]).describe("'search' to find sessions by title, 'read' to get a session transcript"), - query: z.string().optional().describe("Search query to match against session titles (required for search mode)"), - sessionID: z.string().optional().describe("Session ID to read the transcript of (required for read mode)"), - limit: z.number().optional().describe("Maximum number of search results to return (default: 20, max: 50)"), +const Parameters = Schema.Struct({ + mode: Schema.Literals(["search", "read"]).annotate({ + description: "'search' to find sessions by title, 'read' to get a session transcript", + }), + query: Schema.optional(Schema.String).annotate({ + description: "Search query to match against session titles (required for search mode)", + }), + sessionID: Schema.optional(Schema.String).annotate({ + description: "Session ID to read the transcript of (required for read mode)", + }), + limit: Schema.optional(Schema.Number).annotate({ + description: "Maximum number of search results to return (default: 20, max: 50)", + }), }) export const RecallTool = Tool.define( @@ -21,7 +28,7 @@ export const RecallTool = Tool.define( return { description: DESCRIPTION, parameters: Parameters, - execute: (params: z.infer, ctx: Tool.Context) => + execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { if (params.mode === "search") { return yield* Effect.promise(() => search(params, ctx)) diff --git a/packages/opencode/src/tool/warpgrep.ts b/packages/opencode/src/tool/warpgrep.ts index 7ebe4a1b911..dcffa997b6e 100644 --- a/packages/opencode/src/tool/warpgrep.ts +++ b/packages/opencode/src/tool/warpgrep.ts @@ -1,5 +1,4 @@ -import z from "zod" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import * as Tool from "./tool" import { WarpGrepClient } from "@morphllm/morphsdk/tools/warp-grep/client" // kilocode_change import { Instance } from "../project/instance" @@ -12,10 +11,10 @@ import DESCRIPTION from "./warpgrep.txt" // return an error when it is missing. const KILO_WARPGREP_PROXY_URL = "https://api.kilo.ai/api/gateway" -const Parameters = z.object({ - query: z - .string() - .describe("Search query describing what code you are looking for. Be specific and descriptive for best results."), // kilocode_change +const Parameters = Schema.Struct({ + query: Schema.String.annotate({ + description: "Search query describing what code you are looking for. Be specific and descriptive for best results.", // kilocode_change + }), }) export const CodebaseSearchTool = Tool.define( @@ -24,7 +23,7 @@ export const CodebaseSearchTool = Tool.define( return { description: DESCRIPTION, parameters: Parameters, - execute: (params: z.infer, ctx: Tool.Context) => + execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { yield* ctx.ask({ permission: "codebase_search", diff --git a/packages/opencode/test/session/network.test.ts b/packages/opencode/test/session/network.test.ts index 63586932d7a..d3e7c0e9729 100644 --- a/packages/opencode/test/session/network.test.ts +++ b/packages/opencode/test/session/network.test.ts @@ -4,6 +4,7 @@ import { Bus } from "../../src/bus" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { SessionNetwork } from "../../src/session/network" +import { SessionID } from "../../src/session/schema" describe("session.network", () => { test("detects common network disconnect codes", () => { @@ -45,7 +46,7 @@ describe("session.network", () => { directory: tmp.path, fn: async () => { const { promise } = await SessionNetwork.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), message: "Connection refused", abort: new AbortController().signal, }) @@ -64,7 +65,7 @@ describe("session.network", () => { directory: tmp.path, fn: async () => { const { promise } = await SessionNetwork.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), message: "Connection timed out", abort: new AbortController().signal, }) @@ -90,7 +91,7 @@ describe("session.network", () => { try { const { promise } = await SessionNetwork.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), message: "Connection timed out", abort: abort.signal, }) @@ -115,7 +116,7 @@ describe("session.network", () => { fn: async () => { const abort = new AbortController() const { promise } = await SessionNetwork.ask({ - sessionID: "ses_test", + sessionID: SessionID.make("ses_test"), message: "Connection refused", abort: abort.signal, })