refactor(core): simplify location filesystem (#31545)

This commit is contained in:
Dax 2026-06-09 14:28:45 -04:00 committed by GitHub
commit 132ef57272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1048 additions and 1416 deletions

View file

@ -14,7 +14,7 @@ import {
type ProviderMetadata,
type ToolCallPart,
type ToolDefinition,
type ToolResultContentPart,
type ToolContent,
type ToolResultPart,
} from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
@ -321,10 +321,10 @@ const lowerImage = Effect.fn("AnthropicMessages.lowerImage")(function* (part: Me
// Tool results may carry structured text/images. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
item: ToolResultContentPart,
item: ToolContent,
) {
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
const media = yield* ProviderShared.validateMedia(
const media = yield* ProviderShared.validateToolFile(
"Anthropic Messages",
item,
new Set<string>(ProviderShared.IMAGE_MIMES),
@ -344,7 +344,7 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte
// with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
return yield* Effect.forEach(content, lowerToolResultContentItem)
})

View file

@ -269,7 +269,7 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
content.push({ text: item.text })
continue
}
const media = yield* BedrockMedia.lower(item)
const media = yield* BedrockMedia.lower({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
if (!("image" in media))
return yield* ProviderShared.invalidRequest("Bedrock Converse only supports image media in tool results")
content.push(media)

View file

@ -14,7 +14,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolResultContentPart,
type ToolContent,
} from "../schema"
import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { GeminiToolSchema } from "./utils/gemini-tool-schema"
@ -262,7 +262,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
})
continue
}
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text)
parts.push({
functionResponse: {
@ -275,7 +275,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
})
for (const item of content) {
if (item.type === "text") continue
const media = yield* ProviderShared.validateMedia("Gemini", item, IMAGE_MIMES)
const media = yield* ProviderShared.validateToolFile("Gemini", item, IMAGE_MIMES)
parts.push({ inlineData: { mimeType: media.mime, data: media.base64 } })
}
}

View file

@ -14,7 +14,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolResultContentPart,
type ToolContent,
} from "../schema"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options"
@ -201,7 +201,7 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
})
const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (
part: Extract<MediaPart | ToolResultContentPart, { type: "media" }>,
part: MediaPart,
) {
const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES)
return { type: "image_url" as const, image_url: { url: media.dataUrl } }
@ -271,13 +271,15 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m
messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) })
continue
}
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
const text = content.filter((item) => item.type === "text").map((item) => item.text)
messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") })
const media = content.filter(
(item): item is Extract<ToolResultContentPart, { type: "media" }> => item.type === "media",
const files = content.filter((item) => item.type === "file")
images.push(
...(yield* Effect.forEach(files, (item) =>
lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }),
)),
)
images.push(...(yield* Effect.forEach(media, lowerMedia)))
}
return { messages, images }
})

View file

@ -14,7 +14,7 @@ import {
type TextPart,
type ToolCallPart,
type ToolDefinition,
type ToolResultContentPart,
type ToolContent,
type ToolResultPart,
} from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
@ -318,10 +318,10 @@ const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function*
// Tool results may carry structured text/images. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fn("OpenAIResponses.lowerToolResultContentItem")(function* (
item: ToolResultContentPart,
item: ToolContent,
) {
if (item.type === "text") return { type: "input_text" as const, text: item.text }
const media = yield* ProviderShared.validateMedia(
const media = yield* ProviderShared.validateToolFile(
"OpenAI Responses",
item,
new Set<string>(ProviderShared.IMAGE_MIMES),
@ -334,7 +334,7 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")
// compatibility with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
// Preserve the narrowed array element type when compiled through a consumer package.
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
const content: ReadonlyArray<ToolContent> = part.result.value
return yield* Effect.forEach(content, lowerToolResultContentItem)
})

View file

@ -9,6 +9,7 @@ import {
type ContentPart,
type LLMRequest,
type MediaPart,
type ToolFileContent,
type TextPart,
type ToolResultPart,
} from "../schema"
@ -233,6 +234,9 @@ export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function*
return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia
})
export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet<string>) =>
validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes)
export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "")
export const toolResultText = (part: ToolResultPart) => {

View file

@ -39,68 +39,24 @@ export const MediaPart = Schema.Struct({
}).annotate({ identifier: "LLM.Content.Media" })
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
export const ToolResultMediaPart = Schema.Struct({
type: Schema.Literal("media"),
mediaType: Schema.String,
data: Schema.String,
filename: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}).annotate({ identifier: "LLM.ToolResult.Media" })
export type ToolResultMediaPart = Schema.Schema.Type<typeof ToolResultMediaPart>
export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart])
export type ToolResultContentPart = Schema.Schema.Type<typeof ToolResultContentPart>
export class ToolTextContent extends Schema.Class<ToolTextContent>("Tool.TextContent")({
export const ToolTextContent = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
}) {}
}).annotate({ identifier: "Tool.TextContent" })
export type ToolTextContent = typeof ToolTextContent.Type
export const ToolFileSource = Schema.Union([
Schema.Struct({ type: Schema.Literal("data"), data: Schema.String }),
Schema.Struct({ type: Schema.Literal("url"), url: Schema.String }),
Schema.Struct({ type: Schema.Literal("file"), uri: Schema.String }),
]).pipe(Schema.toTaggedUnion("type"))
export type ToolFileSource = Schema.Schema.Type<typeof ToolFileSource>
export class ToolFileContent extends Schema.Class<ToolFileContent>("Tool.FileContent")({
export const ToolFileContent = Schema.Struct({
type: Schema.Literal("file"),
source: ToolFileSource,
uri: Schema.String,
mime: Schema.String,
name: Schema.optional(Schema.String),
}) {}
}).annotate({ identifier: "Tool.FileContent" })
export type ToolFileContent = typeof ToolFileContent.Type
/** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */
export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type"))
export type ToolContent = Schema.Schema.Type<typeof ToolContent>
export const toolText = (value: ConstructorParameters<typeof ToolTextContent>[0]) => new ToolTextContent(value)
export const toolFile = (value: ConstructorParameters<typeof ToolFileContent>[0]) => new ToolFileContent(value)
const inlineData = (uri: string) => {
if (!uri.startsWith("data:")) return undefined
const match = /^data:[^;,]+;base64,(.*)$/s.exec(uri)
if (!match) throw new Error("Tool file data URI must contain raw base64 bytes")
return match[1]!
}
const legacyInlineData = (value: string) => {
const data = inlineData(value)
if (data !== undefined) return data
if (/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return value
throw new Error("Legacy tool-result media must contain raw base64 bytes or a base64 data URI")
}
/** Convert a legacy attachment URI without guessing unknown string semantics. */
export const toolFileSourceFromUri = (uri: string): ToolFileSource => {
const data = inlineData(uri)
if (data !== undefined) return { type: "data", data }
const url = URL.parse(uri)
if (url?.protocol === "file:") return { type: "file", uri }
if (url?.protocol === "http:" || url?.protocol === "https:") return { type: "url", url: uri }
throw new Error(`Unsupported tool file URI: ${uri}`)
}
const isToolResultValue = (value: unknown): value is ToolResultValue =>
isRecord(value) &&
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
@ -122,7 +78,7 @@ export const ToolResultValue = Object.assign(
}),
Schema.Struct({
type: Schema.Literal("content"),
value: Schema.Array(ToolResultContentPart),
value: Schema.Array(ToolContent),
}),
]).annotate({ identifier: "LLM.ToolResult" }),
{
@ -147,34 +103,15 @@ export const ToolOutput = Object.assign(
content: Schema.Array(ToolContent),
}).annotate({ identifier: "LLM.ToolOutput" }),
{
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({
structured,
content: content.map((item) =>
item.type === "text"
? toolText({ type: "text", text: item.text })
: toolFile({ type: "file", source: item.source, mime: item.mime, name: item.name }),
),
}),
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({ structured, content }),
fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
switch (result.type) {
case "json":
return { structured: result.value, content: [] }
case "text":
return { structured: {}, content: [toolText({ type: "text", text: toolResultText(result.value) })] }
return { structured: {}, content: [{ type: "text", text: toolResultText(result.value) }] }
case "content":
return {
structured: {},
content: result.value.map((item) =>
item.type === "text"
? toolText({ type: "text", text: item.text })
: toolFile({
type: "file",
source: { type: "data", data: legacyInlineData(item.data) },
mime: item.mediaType,
name: item.filename,
}),
),
}
return { structured: {}, content: result.value }
case "error":
return undefined
}
@ -183,21 +120,7 @@ export const ToolOutput = Object.assign(
if (output.content.length === 0) return { type: "json", value: output.structured }
if (output.content.length === 1 && output.content[0]?.type === "text")
return { type: "text", value: output.content[0].text }
const unsupported = output.content.find((item) => item.type === "file" && item.source.type !== "data")
if (unsupported?.type === "file")
return {
type: "error",
value: `Tool file source "${unsupported.source.type}" must be materialized to inline data before provider conversion`,
}
return {
type: "content",
value: output.content.map((item) => {
if (item.type === "text") return { type: "text", text: item.text }
if (item.source.type !== "data")
throw new Error("Unmaterialized tool file source reached provider conversion")
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
}),
}
return { type: "content", value: output.content }
},
},
)

View file

@ -5,7 +5,7 @@ import type {
ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType,
} from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
/**
* Schema constraint for tool parameters / success values: no decoding or
@ -245,7 +245,7 @@ const project = (
ToolOutput.make(
toStructuredOutput?.(output) ?? output,
toModelOutput?.({ callID, parameters, output }) ??
(typeof output === "string" ? [toolText({ type: "text", text: output })] : []),
(typeof output === "string" ? [{ type: "text", text: output }] : []),
)
export { ToolFailure }