From 274033cd52464c5fe8eadde8e2b7fdd516b4549f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 10 May 2026 22:59:20 -0400 Subject: [PATCH] Validate prompt messages with Effect Schema (#26796) --- packages/opencode/src/session/message-v2.ts | 33 +++++---------------- packages/opencode/src/session/prompt.ts | 15 ++++++---- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 3eb6f07b82..a8c8dabc86 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -23,7 +23,7 @@ import type { SystemError } from "bun" import type { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" import { Effect, Schema, Types } from "effect" -import { zod, ZodOverride } from "@opencode-ai/core/effect-zod" +import { zod } from "@opencode-ai/core/effect-zod" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema" import { namedSchemaError } from "@/util/named-schema-error" import * as EffectLogger from "@opencode-ai/core/effect/logger" @@ -402,7 +402,7 @@ export const User = Schema.Struct({ .pipe(withStatics((s) => ({ zod: zod(s) }))) export type User = Types.DeepMutable> -const _Part = Schema.Union([ +export const Part = Schema.Union([ TextPart, SubtaskPart, ReasoningPart, @@ -416,22 +416,6 @@ const _Part = Schema.Union([ RetryPart, CompactionPart, ]).annotate({ discriminator: "type", identifier: "Part" }) -export const Part = Object.assign(_Part, { - zod: zod(_Part) as unknown as z.ZodType< - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - >, -}) export type Part = | TextPart | SubtaskPart @@ -573,15 +557,12 @@ export type Assistant = Omit, -}) +export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) export type Info = User | Assistant const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, - info: _Info, + info: Info, }) const RemovedEventSchema = Schema.Struct({ @@ -591,7 +572,7 @@ const RemovedEventSchema = Schema.Struct({ const PartUpdatedEventSchema = Schema.Struct({ sessionID: SessionID, - part: _Part, + part: Part, time: NonNegativeInt, }) @@ -639,8 +620,8 @@ export const Event = { } export const WithParts = Schema.Struct({ - info: _Info, - parts: Schema.Array(_Part), + info: Info, + parts: Schema.Array(Part), }).pipe(withStatics((s) => ({ zod: zod(s) }))) export type WithParts = { info: Info diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5414eba2e5..7f4f608556 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -65,6 +65,9 @@ import { SessionTable } from "./session.sql" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false +const decodeMessageInfo = Schema.decodeUnknownExit(MessageV2.Info) +const decodeMessagePart = Schema.decodeUnknownExit(MessageV2.Part) + const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format. IMPORTANT: @@ -1292,26 +1295,26 @@ NOTE: At any point in time through this workflow you should feel free to ask the const parts = resolvedParts - const parsed = MessageV2.Info.zod.safeParse(info) - if (!parsed.success) { + const parsed = decodeMessageInfo(info, { errors: "all", propertyOrder: "original" }) + if (Exit.isFailure(parsed)) { log.error("invalid user message before save", { sessionID: input.sessionID, messageID: info.id, agent: info.agent, model: info.model, - issues: parsed.error.issues, + cause: Cause.pretty(parsed.cause), }) } parts.forEach((part, index) => { - const p = MessageV2.Part.zod.safeParse(part) - if (p.success) return + const p = decodeMessagePart(part, { errors: "all", propertyOrder: "original" }) + if (Exit.isSuccess(p)) return log.error("invalid user part before save", { sessionID: input.sessionID, messageID: info.id, partID: part.id, partType: part.type, index, - issues: p.error.issues, + cause: Cause.pretty(p.cause), part, }) })