export * as ReadTool from "./read" import { ToolFailure } from "@opencode-ai/llm" import { Effect, Layer, Schema } from "effect" import { FileSystem } from "../filesystem" import { Image } from "../image" import { PermissionV2 } from "../permission" import { Tool } from "./tool" import { Tools } from "./tools" export const name = "read" const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]) const LocationInput = Schema.Struct({ ...FileSystem.ReadInput.fields, offset: FileSystem.ListPageInput.fields.offset.annotate({ description: "The 1-based directory entry or text line offset to start reading from", }), limit: FileSystem.ListPageInput.fields.limit.annotate({ description: "The maximum number of directory entries or text lines to read", }), }) const Input = LocationInput const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage]) export const layer = Layer.effectDiscard( Effect.gen(function* () { const tools = yield* Tools.Service const filesystem = yield* FileSystem.Service const image = yield* Image.Service const permission = yield* PermissionV2.Service yield* tools .register({ [name]: Tool.make({ description: "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page relative to the current location. Absolute paths are accepted only for managed tool-output files.", input: Input, output: Success, toModelOutput: ({ input, output }) => { if (!("type" in output) || output.type !== "binary" || !SUPPORTED_IMAGE_MIMES.has(output.mime)) return [] return [ { type: "text", text: "Image read successfully" }, { type: "file", data: output.content, mime: output.mime, name: input.path }, ] }, execute: (input, context) => { return Effect.gen(function* () { const resolved = yield* filesystem.resolveReadPath(input) yield* permission.assert({ action: name, resources: [resolved.resource], save: ["*"], sessionID: context.sessionID, agent: context.agent, source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, }) if (resolved.type === "directory") return yield* filesystem.listPage(input) const content = yield* filesystem.readTool(input, { offset: input.offset, limit: input.limit, }) if (content.type === "binary" && SUPPORTED_IMAGE_MIMES.has(content.mime)) { return yield* image .normalize(resolved.resource, content) .pipe(Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(content))) } if (content.type === "binary") return yield* Effect.fail(new FileSystem.BinaryFileError(resolved.resource)) return content }).pipe( Effect.mapError((error) => { const message = error instanceof FileSystem.BinaryFileError || error instanceof FileSystem.MediaIngestLimitError || error instanceof Image.DecodeError || error instanceof Image.SizeError ? error.message : `Unable to read ${input.path}` return new ToolFailure({ message }) }), ) }, }), }) .pipe(Effect.orDie) }), )