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<QuestionID>
- Update tests to use SessionID.make() for branded identifiers
This commit is contained in:
Imanol Maiztegui
2026-04-27 18:34:48 +02:00
parent fd8818e48c
commit 24d1f693ef
14 changed files with 196 additions and 133 deletions
@@ -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,
}),
),
}
@@ -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<typeof SessionPrompt.PromptInput.extend> | undefined
let _remotePromptInput: z.ZodObject<any> | 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) {
@@ -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<typeof Event.TurnClose.properties>["reason"]
export type CloseReason = Schema.Schema.Type<typeof CloseReasonSchema>
// ---------------------------------------------------------------------------
// Per-session platform override (telemetry attribution)
@@ -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<typeof Action>
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<typeof Request>
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<typeof Accept>
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())
@@ -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<typeof Params, Meta, never, "suggest">(
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,
})
@@ -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) => {
+11 -8
View File
@@ -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<Schema.Schema.Type<typeof EditorContext>>
// 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" })
+53 -49
View File
@@ -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<typeof Wait>
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<typeof Wait>
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<Wait>
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<void>((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 {
+5 -11
View File
@@ -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<Schema.Schema.Type<typeof PromptInput>, "parts"> & {
export type PromptInput = Omit<Schema.Schema.Type<typeof PromptInput>, "parts" | "editorContext"> & {
parts: PartInputUnion[]
editorContext?: MessageV2.EditorContext
}
export class LoopInput extends Schema.Class<LoopInput>("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<typeof LoopInput>) => 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
+30 -11
View File
@@ -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<T extends MessageV2.Info>(msg: T): Promise<T> {
MessageV2.Info.zod.parse(msg) // kilocode_change
return runPromise((svc) => svc.updateMessage(msg))
+2 -1
View File
@@ -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
+15 -8
View File
@@ -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<typeof Parameters>, ctx: Tool.Context) =>
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
if (params.mode === "search") {
return yield* Effect.promise(() => search(params, ctx))
+6 -7
View File
@@ -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<typeof Parameters>, ctx: Tool.Context) =>
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
yield* ctx.ask({
permission: "codebase_search",
@@ -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,
})