mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-31 19:13:34 +00:00
refactor(core): tag read outputs (#39122)
This commit is contained in:
parent
9a55d125f6
commit
9977ef0160
3 changed files with 37 additions and 9 deletions
|
|
@ -4,7 +4,6 @@ import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin
|
|||
import { dirname } from "path"
|
||||
import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { FileSystem } from "../../filesystem"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Location } from "../../location"
|
||||
import { LocationMutation } from "../../location-mutation"
|
||||
|
|
@ -26,7 +25,11 @@ const LocationInput = Schema.Struct({
|
|||
}),
|
||||
})
|
||||
const Input = LocationInput
|
||||
const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
|
||||
const Output = Schema.Union([
|
||||
ReadToolFileSystem.FileContent,
|
||||
ReadToolFileSystem.TextPage,
|
||||
ReadToolFileSystem.ListPage,
|
||||
])
|
||||
|
||||
export const Plugin = {
|
||||
id: "opencode.tool.read",
|
||||
|
|
@ -107,7 +110,7 @@ export const Plugin = {
|
|||
Effect.catch(() => Effect.void),
|
||||
Effect.catchDefect(() => Effect.void),
|
||||
)
|
||||
if ("encoding" in content && content.encoding === "base64" && !SUPPORTED_IMAGE_MIMES.has(content.mime))
|
||||
if (content.type === "file" && content.encoding === "base64" && !SUPPORTED_IMAGE_MIMES.has(content.mime))
|
||||
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource }))
|
||||
return content
|
||||
}).pipe(
|
||||
|
|
@ -115,7 +118,7 @@ export const Plugin = {
|
|||
// Image base64 reaches the model through content items; avoid a second
|
||||
// unresized copy in model text.
|
||||
const content =
|
||||
"encoding" in output && output.encoding === "base64"
|
||||
output.type === "file" && output.encoding === "base64"
|
||||
? SUPPORTED_IMAGE_MIMES.has(output.mime)
|
||||
? ([
|
||||
{ type: "text", text: "Image read successfully" },
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ export const PageInput = Schema.Struct({
|
|||
})
|
||||
export type PageInput = typeof PageInput.Type
|
||||
|
||||
export const FileContent = Schema.Struct({
|
||||
type: Schema.Literal("file"),
|
||||
...FileSystem.Content.fields,
|
||||
}).annotate({ identifier: "ReadTool.FileContent" })
|
||||
export type FileContent = typeof FileContent.Type
|
||||
|
||||
export class TextPage extends Schema.Class<TextPage>("ReadTool.TextPage")({
|
||||
type: Schema.Literal("text-page"),
|
||||
content: Schema.String,
|
||||
|
|
@ -85,6 +91,7 @@ export class TextPage extends Schema.Class<TextPage>("ReadTool.TextPage")({
|
|||
}) {}
|
||||
|
||||
export class ListPage extends Schema.Class<ListPage>("ReadTool.ListPage")({
|
||||
type: Schema.Literal("list-page"),
|
||||
entries: Schema.Array(FileSystem.Entry),
|
||||
truncated: Schema.Boolean,
|
||||
next: PositiveInt.pipe(Schema.optional),
|
||||
|
|
@ -96,7 +103,7 @@ export interface Interface {
|
|||
path: AbsolutePath,
|
||||
resource: string,
|
||||
page?: PageInput,
|
||||
) => Effect.Effect<FileSystem.Content | TextPage, ReadError>
|
||||
) => Effect.Effect<FileContent | TextPage, ReadError>
|
||||
readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect<ListPage, FSUtil.Error>
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +206,7 @@ export const read = Effect.fn("ReadTool.read")(function* (
|
|||
if (total > MAX_MEDIA_INGEST_BYTES)
|
||||
return yield* Effect.fail(new MediaIngestLimitError({ resource, maximumBytes: MAX_MEDIA_INGEST_BYTES }))
|
||||
return {
|
||||
type: "file" as const,
|
||||
uri: pathToFileURL(real).href,
|
||||
name: path.basename(real),
|
||||
content: Buffer.concat(
|
||||
|
|
@ -223,6 +231,7 @@ export const read = Effect.fn("ReadTool.read")(function* (
|
|||
}
|
||||
text.push(yield* decodeUtf8(resource, decoder))
|
||||
return {
|
||||
type: "file" as const,
|
||||
uri: pathToFileURL(real).href,
|
||||
name: path.basename(real),
|
||||
content: text.join(""),
|
||||
|
|
@ -348,7 +357,12 @@ export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface,
|
|||
.sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1))
|
||||
const selected = visible.slice(offset - 1, offset - 1 + limit)
|
||||
const truncated = offset - 1 + selected.length < visible.length
|
||||
return new ListPage({ entries: selected, truncated, ...(truncated ? { next: offset + selected.length } : {}) })
|
||||
return new ListPage({
|
||||
type: "list-page",
|
||||
entries: selected,
|
||||
truncated,
|
||||
...(truncated ? { next: offset + selected.length } : {}),
|
||||
})
|
||||
})
|
||||
|
||||
const layer = Layer.effect(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { Config } from "@opencode-ai/core/config"
|
|||
import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Image } from "@opencode-ai/core/image"
|
||||
|
|
@ -48,7 +47,8 @@ const readCalls: {
|
|||
const listCalls: ReadToolFileSystem.PageInput[] = []
|
||||
let resolvedType: "file" | "directory" = "file"
|
||||
let resolveFailure: unknown
|
||||
let readResult: FileSystem.Content | ReadToolFileSystem.TextPage = {
|
||||
let readResult: ReadToolFileSystem.FileContent | ReadToolFileSystem.TextPage = {
|
||||
type: "file",
|
||||
uri: "file:///README.md",
|
||||
name: "README.md",
|
||||
content: "hello",
|
||||
|
|
@ -69,7 +69,7 @@ const reader = Layer.succeed(
|
|||
list: (_path, input = {}) =>
|
||||
Effect.sync(() => {
|
||||
listCalls.push(input)
|
||||
return new ReadToolFileSystem.ListPage({ entries: [], truncated: false })
|
||||
return new ReadToolFileSystem.ListPage({ type: "list-page", entries: [], truncated: false })
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
|
@ -181,6 +181,7 @@ describe("ReadTool", () => {
|
|||
resolvedType = "file"
|
||||
resolveFailure = undefined
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///README.md",
|
||||
name: "README.md",
|
||||
content: "hello",
|
||||
|
|
@ -209,6 +210,7 @@ describe("ReadTool", () => {
|
|||
expect(execution.status).toBe("completed")
|
||||
if (execution.status !== "completed") return
|
||||
expect(execution.output).toEqual({
|
||||
type: "file",
|
||||
uri: "file:///README.md",
|
||||
name: "README.md",
|
||||
content: "hello",
|
||||
|
|
@ -253,6 +255,7 @@ describe("ReadTool", () => {
|
|||
Effect.gen(function* () {
|
||||
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///pixel.png",
|
||||
name: "pixel.png",
|
||||
content: png,
|
||||
|
|
@ -305,6 +308,7 @@ describe("ReadTool", () => {
|
|||
source.free()
|
||||
expect(Buffer.byteLength(png)).toBeGreaterThan(50 * 1024)
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///large.png",
|
||||
name: "large.png",
|
||||
content: png,
|
||||
|
|
@ -338,6 +342,7 @@ describe("ReadTool", () => {
|
|||
Effect.gen(function* () {
|
||||
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///pixel.png",
|
||||
name: "pixel.png",
|
||||
content: png,
|
||||
|
|
@ -362,6 +367,7 @@ describe("ReadTool", () => {
|
|||
it.effect("drops undecodable image data from the outcome", () =>
|
||||
Effect.gen(function* () {
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///truncated.png",
|
||||
name: "truncated.png",
|
||||
content: "iVBORw0KGgo=",
|
||||
|
|
@ -393,6 +399,7 @@ describe("ReadTool", () => {
|
|||
const base64 = Buffer.from(source.get_bytes()).toString("base64")
|
||||
source.free()
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///wide.png",
|
||||
name: "wide.png",
|
||||
content: base64,
|
||||
|
|
@ -434,6 +441,7 @@ describe("ReadTool", () => {
|
|||
const base64 = Buffer.from(source.get_bytes()).toString("base64")
|
||||
source.free()
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///wide.png",
|
||||
name: "wide.png",
|
||||
content: base64,
|
||||
|
|
@ -471,6 +479,7 @@ describe("ReadTool", () => {
|
|||
Effect.gen(function* () {
|
||||
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///pixel.png",
|
||||
name: "pixel.png",
|
||||
content: png,
|
||||
|
|
@ -509,6 +518,7 @@ describe("ReadTool", () => {
|
|||
Effect.gen(function* () {
|
||||
const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///pixel.bin",
|
||||
name: "pixel.bin",
|
||||
content: png,
|
||||
|
|
@ -698,6 +708,7 @@ describe("ReadTool", () => {
|
|||
it.effect("rejects unsupported binary discovered by a direct read", () =>
|
||||
Effect.gen(function* () {
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///late-binary",
|
||||
name: "late-binary",
|
||||
content: "AAECAw==",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue