opencode/packages/core/src/tool/read.ts
2026-06-04 23:12:17 -04:00

100 lines
4.2 KiB
TypeScript

export * as ReadTool from "./read"
import { Tool, ToolFailure } from "@opencode-ai/llm"
import { Cause, Effect, Layer, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { NonNegativeInt, PositiveInt } from "../schema"
import { PermissionV2 } from "../permission"
import { ToolOutputStore } from "../tool-output-store"
import { ToolRegistry } from "./registry"
export const name = "read"
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 ResourceInput = Schema.Struct({
resource: Schema.String,
offset: NonNegativeInt.pipe(Schema.optional),
limit: PositiveInt.check(Schema.isLessThanOrEqualTo(ToolOutputStore.MAX_READ_BYTES)).pipe(Schema.optional),
})
const Input = Schema.Union([LocationInput, ResourceInput])
const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage, ToolOutputStore.Page])
const definition = Tool.make({
description:
"Read a text or binary file, page through a large UTF-8 text file by line offset, list a directory page relative to the current location, or page through a managed tool-output resource by opaque URI.",
parameters: Input,
success: Success,
})
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const filesystem = yield* FileSystem.Service
const resources = yield* ToolOutputStore.Service
yield* registry.contribute((editor) =>
editor.set(name, {
tool: definition,
execute: ({ parameters, sessionID, assertPermission }) => {
const input = parameters
return Effect.gen(function* () {
if ("resource" in input)
return yield* resources.read({ sessionID, uri: input.resource, offset: input.offset, limit: input.limit })
const resolved = yield* filesystem.resolveReadPath(input)
if (resolved.type === "directory") {
const { offset, limit } = input
const target = resolved.target
yield* assertPermission({ action: name, resources: [target.resource], save: ["*"] })
const final = yield* filesystem.resolveReadPath(input)
if (
final.type !== "directory" ||
final.target.resource !== target.resource ||
final.target.real !== target.real
)
return yield* Effect.die(new Error("Directory changed after permission approval"))
return yield* filesystem.listPageResolved(final.target, { offset, limit })
}
const target = resolved.target
yield* assertPermission({
action: name,
resources: [target.resource],
save: ["*"],
})
const final = yield* filesystem.resolveReadPath(input)
if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
return yield* Effect.die(new Error("File changed after permission approval"))
if (
final.target.size > FileSystem.MAX_READ_BYTES ||
input.offset !== undefined ||
input.limit !== undefined
)
return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(
new ToolFailure({
message: `Unable to read ${"resource" in input ? input.resource : input.path}`,
error: Cause.squash(cause),
}),
),
),
)
},
}),
)
}),
)
export const locationLayer = layer.pipe(
Layer.provideMerge(ToolRegistry.defaultLayer),
Layer.provideMerge(FileSystem.locationLayer),
Layer.provideMerge(PermissionV2.locationLayer),
Layer.provideMerge(ToolOutputStore.defaultLayer),
)