refactor(core): move database schema ownership (#29068)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dax
2026-05-30 21:08:38 -04:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent 6bcb9cb9bb
commit 7f571d36ea
390 changed files with 11127 additions and 9164 deletions
+88 -83
View File
@@ -1,4 +1,5 @@
import path from "path"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import os from "os"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
@@ -7,11 +8,10 @@ import { SessionRevert } from "./revert"
import * as Session from "./session"
import { Agent } from "../agent/agent"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { type Tool as AITool, tool, jsonSchema } from "ai"
import type { JSONSchema7 } from "@ai-sdk/provider"
import { SessionCompaction } from "./compaction"
import { Bus } from "../bus"
import { SystemPrompt } from "./system"
import { Instruction } from "./instruction"
import { Plugin } from "../plugin"
@@ -48,15 +48,15 @@ import { TaskTool, type TaskPromptOps } from "@/tool/task"
import { SessionRunState } from "./run-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionEvent } from "@opencode-ai/core/session-event"
import { Database } from "@opencode-ai/core/database/database"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session-prompt"
import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
import { Reference } from "@/reference/reference"
import * as DateTime from "effect/DateTime"
import { eq } from "@/storage/db"
import * as Database from "@/storage/db"
import { SessionTable } from "./session.sql"
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { referencePromptMetadata, referenceTextPart } from "./prompt/reference"
import { SessionReminders } from "./reminders"
import { SessionTools } from "./tools"
@@ -65,8 +65,8 @@ import { LLMEvent } from "@opencode-ai/llm"
// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
const decodeMessageInfo = Schema.decodeUnknownExit(MessageV2.Info)
const decodeMessagePart = Schema.decodeUnknownExit(MessageV2.Part)
const decodeMessageInfo = Schema.decodeUnknownExit(SessionLegacy.Info)
const decodeMessagePart = Schema.decodeUnknownExit(SessionLegacy.Part)
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
@@ -81,7 +81,7 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
const log = Log.create({ service: "session.prompt" })
const elog = EffectLogger.create({ service: "session.prompt" })
function isOrphanedInterruptedTool(part: MessageV2.ToolPart) {
function isOrphanedInterruptedTool(part: SessionLegacy.ToolPart) {
// cleanup() marks abandoned tool_use blocks this way after retries/aborts.
// They are not pending work and must not trigger an assistant-prefill request.
return part.state.status === "error" && part.state.metadata?.interrupted === true
@@ -89,10 +89,10 @@ function isOrphanedInterruptedTool(part: MessageV2.ToolPart) {
export interface Interface {
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts, Session.BusyError>
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
readonly prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
readonly loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
readonly command: (input: CommandInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
}
@@ -101,7 +101,6 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const bus = yield* Bus.Service
const status = yield* SessionStatus.Service
const sessions = yield* Session.Service
const agents = yield* Agent.Service
@@ -129,6 +128,8 @@ export const layer = Layer.effect(
const references = yield* Reference.Service
const events = yield* EventV2Bridge.Service
const flags = yield* RuntimeFlags.Service
const database = yield* Database.Service
const { db } = database
const ops = Effect.fn("SessionPrompt.ops")(function* () {
return {
cancel: (sessionID: SessionID) => cancel(sessionID),
@@ -240,14 +241,14 @@ export const layer = Layer.effect(
const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
session: Session.Info
history: MessageV2.WithParts[]
providerID: ProviderID
modelID: ModelID
history: SessionLegacy.WithParts[]
providerID: ProviderV2.ID
modelID: ProviderV2.ModelID
}) {
if (input.session.parentID) return
if (!Session.isDefaultTitle(input.session.title)) return
const real = (m: MessageV2.WithParts) =>
const real = (m: SessionLegacy.WithParts) =>
m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
const idx = input.history.findIndex(real)
if (idx === -1) return
@@ -258,7 +259,7 @@ export const layer = Layer.effect(
if (!firstUser || firstUser.info.role !== "user") return
const firstInfo = firstUser.info
const subtasks = firstUser.parts.filter((p): p is MessageV2.SubtaskPart => p.type === "subtask")
const subtasks = firstUser.parts.filter((p): p is SessionLegacy.SubtaskPart => p.type === "subtask")
const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
const ag = yield* agents.get("title")
@@ -301,19 +302,19 @@ export const layer = Layer.effect(
})
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
task: MessageV2.SubtaskPart
task: SessionLegacy.SubtaskPart
model: Provider.Model
lastUser: MessageV2.User
lastUser: SessionLegacy.User
sessionID: SessionID
session: Session.Info
msgs: MessageV2.WithParts[]
msgs: SessionLegacy.WithParts[]
}) {
const { task, model, lastUser, sessionID, session, msgs } = input
const ctx = yield* InstanceState.context
const promptOps = yield* ops()
const { task: taskTool } = yield* registry.named()
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({
const assistantMessage: SessionLegacy.Assistant = yield* sessions.updateMessage({
id: MessageID.ascending(),
role: "assistant",
parentID: lastUser.id,
@@ -328,7 +329,7 @@ export const layer = Layer.effect(
providerID: taskModel.providerID,
time: { created: Date.now() },
})
let part: MessageV2.ToolPart = yield* sessions.updatePart({
let part: SessionLegacy.ToolPart = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID,
@@ -363,7 +364,7 @@ export const layer = Layer.effect(
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
throw error
}
@@ -384,7 +385,7 @@ export const layer = Layer.effect(
...part,
type: "tool",
state: { ...part.state, ...val },
} satisfies MessageV2.ToolPart)
} satisfies SessionLegacy.ToolPart)
}),
ask: (req: any) =>
permission
@@ -418,7 +419,7 @@ export const layer = Layer.effect(
metadata: part.state.metadata,
input: part.state.input,
},
} satisfies MessageV2.ToolPart)
} satisfies SessionLegacy.ToolPart)
}
}),
),
@@ -453,7 +454,7 @@ export const layer = Layer.effect(
attachments,
time: { ...part.state.time, end: Date.now() },
},
} satisfies MessageV2.ToolPart)
} satisfies SessionLegacy.ToolPart)
}
if (!result) {
@@ -469,12 +470,12 @@ export const layer = Layer.effect(
metadata: part.state.status === "pending" ? undefined : part.state.metadata,
input: part.state.input,
},
} satisfies MessageV2.ToolPart)
} satisfies SessionLegacy.ToolPart)
}
if (!task.command) return
const summaryUserMsg: MessageV2.User = {
const summaryUserMsg: SessionLegacy.User = {
id: MessageID.ascending(),
sessionID,
role: "user",
@@ -490,7 +491,7 @@ export const layer = Layer.effect(
type: "text",
text: "Summarize the task tool output above and continue with your task.",
synthetic: true,
} satisfies MessageV2.TextPart)
} satisfies SessionLegacy.TextPart)
})
const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput, ready?: Latch.Latch) {
@@ -508,11 +509,11 @@ export const layer = Layer.effect(
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
const userMsg: MessageV2.User = {
const userMsg: SessionLegacy.User = {
id: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID,
time: { created: Date.now() },
@@ -521,7 +522,7 @@ export const layer = Layer.effect(
model: { providerID: model.providerID, modelID: model.modelID },
}
yield* sessions.updateMessage(userMsg)
const userPart: MessageV2.Part = {
const userPart: SessionLegacy.Part = {
type: "text",
id: PartID.ascending(),
messageID: userMsg.id,
@@ -531,7 +532,7 @@ export const layer = Layer.effect(
}
yield* sessions.updatePart(userPart)
const msg: MessageV2.Assistant = {
const msg: SessionLegacy.Assistant = {
id: MessageID.ascending(),
sessionID: input.sessionID,
parentID: userMsg.id,
@@ -547,7 +548,7 @@ export const layer = Layer.effect(
}
yield* sessions.updateMessage(msg)
const started = Date.now()
const part: MessageV2.ToolPart = {
const part: SessionLegacy.ToolPart = {
type: "tool",
id: PartID.ascending(),
messageID: msg.id,
@@ -653,8 +654,8 @@ export const layer = Layer.effect(
})
const getModel = Effect.fn("SessionPrompt.getModel")(function* (
providerID: ProviderID,
modelID: ModelID,
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
sessionID: SessionID,
) {
const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
@@ -662,7 +663,7 @@ export const layer = Layer.effect(
const err = Cause.squash(exit.cause)
if (Provider.ModelNotFoundError.isInstance(err)) {
const hint = err.suggestions?.length ? ` Did you mean: ${err.suggestions.join(", ")}?` : ""
yield* bus.publish(Session.Event.Error, {
yield* events.publish(Session.Event.Error, {
sessionID,
error: new NamedError.Unknown({
message: `Model not found: ${err.providerID}/${err.modelID}.${hint}`,
@@ -673,13 +674,16 @@ export const layer = Layer.effect(
})
const currentModel = Effect.fnUntraced(function* (sessionID: SessionID) {
const current = Database.use((db) =>
db.select({ model: SessionTable.model }).from(SessionTable).where(eq(SessionTable.id, sessionID)).get(),
)
const current = yield* db
.select({ model: SessionTable.model })
.from(SessionTable)
.where(eq(SessionTable.id, sessionID))
.get()
.pipe(Effect.orDie)
if (current?.model) {
return {
providerID: ProviderID.make(current.model.providerID),
modelID: ModelID.make(current.model.id),
providerID: ProviderV2.ID.make(current.model.providerID),
modelID: ProviderV2.ModelID.make(current.model.id),
...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}),
}
}
@@ -697,17 +701,16 @@ export const layer = Layer.effect(
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
const current = Database.use((db) =>
db
.select({ agent: SessionTable.agent, model: SessionTable.model })
.from(SessionTable)
.where(eq(SessionTable.id, input.sessionID))
.get(),
)
const current = yield* db
.select({ agent: SessionTable.agent, model: SessionTable.model })
.from(SessionTable)
.where(eq(SessionTable.id, input.sessionID))
.get()
.pipe(Effect.orDie)
const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID))
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
const full =
@@ -718,7 +721,7 @@ export const layer = Layer.effect(
: undefined
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
const info: MessageV2.User = {
const info: SessionLegacy.User = {
id: input.messageID ?? MessageID.ascending(),
role: "user",
sessionID: input.sessionID,
@@ -759,8 +762,8 @@ export const layer = Layer.effect(
yield* Effect.addFinalizer(() => instruction.clear(info.id))
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
type Draft<T> = T extends SessionLegacy.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<SessionLegacy.Part>): SessionLegacy.Part => ({
...part,
id: part.id ? PartID.make(part.id) : PartID.ascending(),
})
@@ -789,14 +792,14 @@ export const layer = Layer.effect(
})
})
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<MessageV2.Part>[]> = Effect.fn(
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionLegacy.Part>[]> = Effect.fn(
"SessionPrompt.resolveUserPart",
)(function* (part) {
if (part.type === "file") {
if (part.source?.type === "resource") {
const { clientName, uri } = part.source
log.info("mcp resource", { clientName, uri, mime: part.mime })
const pieces: Draft<MessageV2.Part>[] = [
const pieces: Draft<SessionLegacy.Part>[] = [
{
messageID: info.id,
sessionID: input.sessionID,
@@ -916,7 +919,7 @@ export const layer = Layer.effect(
if (end) limit = end - (offset - 1)
}
const args = { filePath: filepath, offset, limit }
const pieces: Draft<MessageV2.Part>[] = [
const pieces: Draft<SessionLegacy.Part>[] = [
...(referenceContext
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
: []),
@@ -958,7 +961,7 @@ export const layer = Layer.effect(
const error = Cause.squash(exit.cause)
log.error("failed to read file", { error })
const message = error instanceof Error ? error.message : String(error)
yield* bus.publish(Session.Event.Error, {
yield* events.publish(Session.Event.Error, {
sessionID: input.sessionID,
error: new NamedError.Unknown({ message }).toObject(),
})
@@ -980,7 +983,7 @@ export const layer = Layer.effect(
const error = Cause.squash(exit.cause)
log.error("failed to read directory", { error })
const message = error instanceof Error ? error.message : String(error)
yield* bus.publish(Session.Event.Error, {
yield* events.publish(Session.Event.Error, {
sessionID: input.sessionID,
error: new NamedError.Unknown({ message }).toObject(),
})
@@ -1212,7 +1215,7 @@ export const layer = Layer.effect(
return { info, parts }
}, Effect.scoped)
const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error> = Effect.fn(
const prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error> = Effect.fn(
"SessionPrompt.prompt",
)(function* (input: PromptInput) {
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
@@ -1241,7 +1244,7 @@ export const layer = Layer.effect(
throw new Error("Impossible")
})
const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.run")(
function* (sessionID: SessionID) {
const ctx = yield* InstanceState.context
const slog = elog.with({ sessionID })
@@ -1253,7 +1256,9 @@ export const layer = Layer.effect(
yield* status.set(sessionID, { type: "busy" })
yield* slog.info("loop", { step })
let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe(
Effect.provideService(Database.Service, database),
)
const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs)
@@ -1277,7 +1282,7 @@ export const layer = Layer.effect(
lastUser.id < lastAssistant.id
) {
const orphan = lastAssistantMsg?.parts.find(
(part): part is MessageV2.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
(part): part is SessionLegacy.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
)
if (orphan) {
yield* slog.warn("loop exit with orphaned interrupted tool", {
@@ -1333,7 +1338,7 @@ export const layer = Layer.effect(
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID, error: error.toObject() })
throw error
}
const maxSteps = agent.steps ?? Infinity
@@ -1344,7 +1349,7 @@ export const layer = Layer.effect(
Effect.provideService(Session.Service, sessions),
)
const msg: MessageV2.Assistant = {
const msg: SessionLegacy.Assistant = {
id: MessageID.ascending(),
parentID: lastUser.id,
role: "assistant",
@@ -1464,7 +1469,7 @@ export const layer = Layer.effect(
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
if (finished && !handle.message.error) {
if (format.type === "json_schema") {
handle.message.error = new MessageV2.StructuredOutputError({
handle.message.error = new SessionLegacy.StructuredOutputError({
message: "Model did not produce structured output",
retries: 0,
}).toObject()
@@ -1497,13 +1502,13 @@ export const layer = Layer.effect(
},
)
const loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
input: LoopInput,
) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
})
const shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts, Session.BusyError> = Effect.fn(
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
"SessionPrompt.shell",
)(function* (input: ShellInput) {
const ready = yield* Latch.make()
@@ -1517,7 +1522,7 @@ export const layer = Layer.effect(
const available = (yield* commands.list()).map((c) => c.name)
const hint = available.length ? ` Available commands: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
const agentName = cmd.agent ?? input.agent
@@ -1578,7 +1583,7 @@ export const layer = Layer.effect(
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
yield* events.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
@@ -1618,7 +1623,7 @@ export const layer = Layer.effect(
parts,
variant: input.variant,
})
yield* bus.publish(Command.Event.Executed, {
yield* events.publish(Command.Event.Executed, {
name: input.command,
sessionID: input.sessionID,
arguments: input.arguments,
@@ -1661,21 +1666,21 @@ export const defaultLayer = Layer.suspend(() =>
Layer.provide(Image.defaultLayer),
Layer.provide(
Layer.mergeAll(
EventV2Bridge.defaultLayer,
Agent.defaultLayer,
Database.defaultLayer,
SystemPrompt.defaultLayer,
LLM.defaultLayer,
Reference.defaultLayer,
Bus.layer,
CrossSpawnSpawner.defaultLayer,
RuntimeFlags.defaultLayer,
EventV2Bridge.defaultLayer,
),
),
),
)
const ModelRef = Schema.Struct({
providerID: ProviderID,
modelID: ModelID,
providerID: ProviderV2.ID,
modelID: ProviderV2.ModelID,
})
export const PromptInput = Schema.Struct({
@@ -1688,15 +1693,15 @@ export const PromptInput = Schema.Struct({
description:
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
}),
format: Schema.optional(MessageV2.Format),
format: Schema.optional(SessionLegacy.Format),
system: Schema.optional(Schema.String),
variant: Schema.optional(Schema.String),
parts: Schema.Array(
Schema.Union([
MessageV2.TextPartInput,
MessageV2.FilePartInput,
MessageV2.AgentPartInput,
MessageV2.SubtaskPartInput,
SessionLegacy.TextPartInput,
SessionLegacy.FilePartInput,
SessionLegacy.AgentPartInput,
SessionLegacy.SubtaskPartInput,
]).annotate({ discriminator: "type" }),
),
})
@@ -1735,7 +1740,7 @@ export const CommandInput = Schema.Struct({
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(MessageV2.FilePartSource),
source: Schema.optional(SessionLegacy.FilePartSource),
}),
]).annotate({ discriminator: "type" }),
),