From e3134a2a995f3e30b9a21a0546ace1e8c4d3cc5d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Apr 2026 19:28:46 -0400 Subject: [PATCH] refactor(session): align prompt input types with their schemas (#25178) --- .../instance/httpapi/handlers/session.ts | 8 ++--- packages/opencode/src/session/prompt.ts | 31 ++++++------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts index c4def3e742..91afd0045f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts @@ -269,7 +269,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", promptSvc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID, - } as unknown as SessionPrompt.PromptInput), + }), ), ), ).pipe( @@ -288,7 +288,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", yield* Effect.sync(() => { bridge.fork( promptSvc - .prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput) + .prompt({ ...ctx.payload, sessionID: ctx.params.sessionID }) .pipe( Effect.catchCause((error) => Effect.sync(() => { @@ -309,14 +309,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", params: { sessionID: SessionID } payload: typeof CommandPayload.Type }) { - return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput) + return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID }) }) const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: { params: { sessionID: SessionID } payload: typeof ShellPayload.Type }) { - return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput) + return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID }) }) const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index c4d8673222..155b86b583 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -45,7 +45,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "@/tool/truncate" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util/process" -import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema } from "effect" +import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" import * as EffectLogger from "@opencode-ai/core/effect/logger" @@ -127,7 +127,7 @@ export const layer = Layer.effect( const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) { const ctx = yield* InstanceState.context - const parts: PromptInput["parts"] = [{ type: "text", text: template }] + const parts: Types.DeepMutable = [{ type: "text", text: template }] const files = ConfigMarkdown.files(template) const seen = new Set() yield* Effect.forEach( @@ -1012,7 +1012,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the case "file:": { log.info("file", { mime: part.mime }) const filepath = fileURLToPath(part.url) - if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory" + const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime const { read } = yield* registry.named() const execRead = (args: Parameters[0], extra?: Tool.Context["extra"]) => { @@ -1031,7 +1031,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()))) } - if (part.mime === "text/plain") { + if (mime === "text/plain") { let offset: number | undefined let limit: number | undefined const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") } @@ -1089,7 +1089,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the })), ) } else { - pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID }) + pieces.push({ ...part, mime, messageID: info.id, sessionID: input.sessionID }) } } else { const error = Cause.squash(exit.cause) @@ -1110,7 +1110,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the return pieces } - if (part.mime === "application/x-directory") { + if (mime === "application/x-directory") { const args = { filePath: filepath } const exit = yield* execRead(args).pipe(Effect.exit) if (Exit.isFailure(exit)) { @@ -1146,7 +1146,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the synthetic: true, text: exit.value.output, }, - { ...part, messageID: info.id, sessionID: input.sessionID }, + { ...part, mime, messageID: info.id, sessionID: input.sessionID }, ] } @@ -1164,9 +1164,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the sessionID: input.sessionID, type: "file", url: - `data:${part.mime};base64,` + + `data:${mime};base64,` + Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"), - mime: part.mime, + mime, filename: part.filename!, source: part.source, }, @@ -1700,18 +1700,7 @@ export const PromptInput = Schema.Struct({ ]).annotate({ discriminator: "type" }), ), }).pipe(withStatics((s) => ({ zod: zod(s) }))) -// `z.discriminatedUnion` erases the discriminated members' shapes back to -// `{}` when walked from the generic `z.ZodType` input. Restore the precise -// `parts` type from the exported Schema input types so callers see a proper -// tagged union. -type PartInputUnion = - | MessageV2.TextPartInput - | MessageV2.FilePartInput - | MessageV2.AgentPartInput - | MessageV2.SubtaskPartInput -export type PromptInput = Omit, "parts"> & { - parts: PartInputUnion[] -} +export type PromptInput = Schema.Schema.Type export class LoopInput extends Schema.Class("SessionPrompt.LoopInput")({ sessionID: SessionID,