fix(core): resolve prompt attachment mime types
This commit is contained in:
parent
18a419e634
commit
850a0dfe7c
8 changed files with 86 additions and 12 deletions
|
|
@ -305,7 +305,6 @@ export type SessionsPromptInput = {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly description?: string
|
readonly description?: string
|
||||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
|
@ -324,7 +323,6 @@ export type SessionsPromptInput = {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly description?: string
|
readonly description?: string
|
||||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
|
@ -343,7 +341,6 @@ export type SessionsPromptInput = {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly description?: string
|
readonly description?: string
|
||||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
|
@ -362,7 +359,6 @@ export type SessionsPromptInput = {
|
||||||
readonly text: string
|
readonly text: string
|
||||||
readonly files?: ReadonlyArray<{
|
readonly files?: ReadonlyArray<{
|
||||||
readonly uri: string
|
readonly uri: string
|
||||||
readonly mime: string
|
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
readonly description?: string
|
readonly description?: string
|
||||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ModelV2 } from "./model"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { SessionMessage } from "./session/message"
|
import { SessionMessage } from "./session/message"
|
||||||
import { Prompt } from "./session/prompt"
|
import { Prompt } from "./session/prompt"
|
||||||
|
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { SessionProjector } from "./session/projector"
|
import { SessionProjector } from "./session/projector"
|
||||||
|
|
@ -32,6 +33,7 @@ import { SessionInput } from "./session/input"
|
||||||
import { Snapshot } from "./snapshot"
|
import { Snapshot } from "./snapshot"
|
||||||
import { SessionRevert } from "./session/revert"
|
import { SessionRevert } from "./session/revert"
|
||||||
import { Revert } from "@opencode-ai/schema/revert"
|
import { Revert } from "@opencode-ai/schema/revert"
|
||||||
|
import { FSUtil } from "./fs-util"
|
||||||
|
|
||||||
export const RevertState = Revert.State
|
export const RevertState = Revert.State
|
||||||
export type RevertState = Revert.State
|
export type RevertState = Revert.State
|
||||||
|
|
@ -137,7 +139,7 @@ export interface Interface {
|
||||||
readonly prompt: (input: {
|
readonly prompt: (input: {
|
||||||
id?: SessionMessage.ID
|
id?: SessionMessage.ID
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
prompt: Prompt
|
prompt: PromptInput.Prompt
|
||||||
delivery?: SessionInput.Delivery
|
delivery?: SessionInput.Delivery
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError>
|
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError>
|
||||||
|
|
@ -349,13 +351,14 @@ export const layer = Layer.unwrap(
|
||||||
Effect.uninterruptible(
|
Effect.uninterruptible(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* result.get(input.sessionID)
|
yield* result.get(input.sessionID)
|
||||||
|
const prompt = resolvePrompt(input.prompt)
|
||||||
const messageID = input.id ?? SessionMessage.ID.create()
|
const messageID = input.id ?? SessionMessage.ID.create()
|
||||||
const delivery = input.delivery ?? "steer"
|
const delivery = input.delivery ?? "steer"
|
||||||
const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery }
|
const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
|
||||||
const admitted = yield* SessionInput.admit(db, events, {
|
const admitted = yield* SessionInput.admit(db, events, {
|
||||||
id: messageID,
|
id: messageID,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
prompt: input.prompt,
|
prompt,
|
||||||
delivery,
|
delivery,
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.catchDefect((defect) =>
|
Effect.catchDefect((defect) =>
|
||||||
|
|
@ -452,3 +455,17 @@ export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(ProjectV2.defaultLayer),
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.orDie,
|
Layer.orDie,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resolvePrompt = (input: PromptInput.Prompt) =>
|
||||||
|
Prompt.make({
|
||||||
|
text: input.text,
|
||||||
|
agents: input.agents,
|
||||||
|
files: input.files?.map((file) => {
|
||||||
|
const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1]
|
||||||
|
const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri)
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,27 @@ describe("SessionV2.prompt", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("resolves attachment MIME before admission", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* setup
|
||||||
|
const session = yield* SessionV2.Service
|
||||||
|
|
||||||
|
const message = yield* session.prompt({
|
||||||
|
sessionID,
|
||||||
|
prompt: {
|
||||||
|
text: "Inspect this image",
|
||||||
|
files: [{ uri: "data:image/png;base64,aGVsbG8=", name: "image.png" }],
|
||||||
|
},
|
||||||
|
resume: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(message.prompt.files).toEqual([
|
||||||
|
{ uri: "data:image/png;base64,aGVsbG8=", name: "image.png", mime: "image/png" },
|
||||||
|
])
|
||||||
|
expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("streams durable Session events after an aggregate sequence", () =>
|
it.effect("streams durable Session events after an aggregate sequence", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
yield* setup
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { SessionInput } from "@opencode-ai/schema/session-input"
|
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||||
import { Prompt } from "@opencode-ai/schema/prompt"
|
import { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||||
import { Session } from "@opencode-ai/schema/session"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
import { Project } from "@opencode-ai/schema/project"
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema"
|
||||||
|
|
@ -192,7 +192,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||||
params: { sessionID: Session.ID },
|
params: { sessionID: Session.ID },
|
||||||
payload: Schema.Struct({
|
payload: Schema.Struct({
|
||||||
id: SessionMessage.ID.pipe(Schema.optional),
|
id: SessionMessage.ID.pipe(Schema.optional),
|
||||||
prompt: Prompt,
|
prompt: PromptInput.Prompt,
|
||||||
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||||
resume: Schema.Boolean.pipe(Schema.optional),
|
resume: Schema.Boolean.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,5 @@ export { PtyTicket } from "./pty-ticket"
|
||||||
export { Question } from "./question"
|
export { Question } from "./question"
|
||||||
export { Workspace } from "./workspace"
|
export { Workspace } from "./workspace"
|
||||||
export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt"
|
export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt"
|
||||||
|
export { PromptInput } from "./prompt-input"
|
||||||
export * from "./schema"
|
export * from "./schema"
|
||||||
|
|
|
||||||
26
packages/schema/src/prompt-input.ts
Normal file
26
packages/schema/src/prompt-input.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
export * as PromptInput from "./prompt-input"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { AgentAttachment, Source } from "./prompt"
|
||||||
|
import { optional, statics } from "./schema"
|
||||||
|
|
||||||
|
export interface FileAttachment extends Schema.Schema.Type<typeof FileAttachment> {}
|
||||||
|
export const FileAttachment = Schema.Struct({
|
||||||
|
uri: Schema.String,
|
||||||
|
name: Schema.String.pipe(optional),
|
||||||
|
description: Schema.String.pipe(optional),
|
||||||
|
source: Source.pipe(optional),
|
||||||
|
})
|
||||||
|
.annotate({ identifier: "PromptInput.FileAttachment" })
|
||||||
|
.pipe(
|
||||||
|
statics((schema) => ({
|
||||||
|
create: (input: FileAttachment) => schema.make(input),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
|
||||||
|
export const Prompt = Schema.Struct({
|
||||||
|
text: Schema.String,
|
||||||
|
files: Schema.Array(FileAttachment).pipe(optional),
|
||||||
|
agents: Schema.Array(AgentAttachment).pipe(optional),
|
||||||
|
}).annotate({ identifier: "PromptInput" })
|
||||||
|
|
@ -142,7 +142,7 @@ import type {
|
||||||
ProjectListResponses,
|
ProjectListResponses,
|
||||||
ProjectUpdateErrors,
|
ProjectUpdateErrors,
|
||||||
ProjectUpdateResponses,
|
ProjectUpdateResponses,
|
||||||
Prompt,
|
PromptInput,
|
||||||
ProviderAuthErrors,
|
ProviderAuthErrors,
|
||||||
ProviderAuthResponses,
|
ProviderAuthResponses,
|
||||||
ProviderListErrors,
|
ProviderListErrors,
|
||||||
|
|
@ -5621,7 +5621,7 @@ export class Session3 extends HeyApiClient {
|
||||||
parameters: {
|
parameters: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
id?: string
|
id?: string
|
||||||
prompt?: Prompt
|
prompt?: PromptInput
|
||||||
delivery?: "steer" | "queue"
|
delivery?: "steer" | "queue"
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2700,6 +2700,12 @@ export type SessionNotFoundError = {
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PromptInput = {
|
||||||
|
text: string
|
||||||
|
files?: Array<PromptInputFileAttachment>
|
||||||
|
agents?: Array<PromptAgentAttachment>
|
||||||
|
}
|
||||||
|
|
||||||
export type ConflictError = {
|
export type ConflictError = {
|
||||||
_tag: "ConflictError"
|
_tag: "ConflictError"
|
||||||
message: string
|
message: string
|
||||||
|
|
@ -3814,6 +3820,13 @@ export type SessionV2Info = {
|
||||||
revert?: RevertState
|
revert?: RevertState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PromptInputFileAttachment = {
|
||||||
|
uri: string
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
source?: PromptSource
|
||||||
|
}
|
||||||
|
|
||||||
export type SessionInputAdmitted = {
|
export type SessionInputAdmitted = {
|
||||||
admittedSeq: number
|
admittedSeq: number
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -12091,7 +12104,7 @@ export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V
|
||||||
export type V2SessionPromptData = {
|
export type V2SessionPromptData = {
|
||||||
body: {
|
body: {
|
||||||
id?: string
|
id?: string
|
||||||
prompt: Prompt
|
prompt: PromptInput
|
||||||
delivery?: "steer" | "queue"
|
delivery?: "steer" | "queue"
|
||||||
resume?: boolean
|
resume?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue