Compare commits

...

4 Commits

Author SHA1 Message Date
Kit Langton cbd8f750a1 refactor(config): parse commands with Effect Schema 2026-05-10 22:50:30 -04:00
Kit Langton 983c482169 refactor(session): inline MessageV2 schemas after zod removal 2026-05-10 22:50:27 -04:00
Kit Langton 8316ef88ad refactor(session): drop MessageV2 zod compatibility statics 2026-05-10 22:50:27 -04:00
Kit Langton a2458f49ae refactor(session): validate prompt messages with Effect Schema 2026-05-10 22:50:27 -04:00
3 changed files with 24 additions and 40 deletions
+8 -8
View File
@@ -1,12 +1,10 @@
export * as ConfigCommand from "./command"
import * as Log from "@opencode-ai/core/util/log"
import { Schema } from "effect"
import { Cause, Exit, Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
import { Glob } from "@opencode-ai/core/util/glob"
import { Bus } from "@/bus"
import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@opencode-ai/core/schema"
import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error"
import * as ConfigMarkdown from "./markdown"
@@ -20,10 +18,12 @@ export const Info = Schema.Struct({
agent: Schema.optional(Schema.String),
model: Schema.optional(ConfigModelID),
subtask: Schema.optional(Schema.Boolean),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
})
export type Info = Schema.Schema.Type<typeof Info>
const decodeInfo = Schema.decodeUnknownExit(Info)
export async function load(dir: string) {
const result: Record<string, Info> = {}
for (const item of await Glob.scan("{command,commands}/**/*.md", {
@@ -51,12 +51,12 @@ export async function load(dir: string) {
...md.data,
template: md.content.trim(),
}
const parsed = Info.zod.safeParse(config)
if (parsed.success) {
result[config.name] = parsed.data
const parsed = decodeInfo(config, { errors: "all", propertyOrder: "original" })
if (Exit.isSuccess(parsed)) {
result[config.name] = parsed.value
continue
}
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
throw new InvalidError({ path: item, message: Cause.pretty(parsed.cause) }, { cause: Cause.squash(parsed.cause) })
}
return result
}
+7 -26
View File
@@ -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<Schema.Schema.Type<typeof User>>
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<Types.DeepMutable<Schema.Schema.Type<typeof Assista
error?: AssistantError
}
const _Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
export const Info = Object.assign(_Info, {
zod: zod(_Info) as unknown as z.ZodType<User | Assistant>,
})
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
+9 -6
View File
@@ -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,
})
})