2026-06-04 03:02:17 +00:00
export * as ReadTool from "./read"
2026-07-07 23:56:47 +00:00
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
2026-07-02 00:50:10 +00:00
import { dirname } from "path"
2026-07-15 19:10:38 +00:00
import { ToolFailure } from "@opencode-ai/ai"
2026-07-03 13:03:53 +00:00
import { Effect , Schema } from "effect"
2026-06-04 03:02:17 +00:00
import { FileSystem } from "../filesystem"
2026-07-02 00:50:10 +00:00
import { FSUtil } from "../fs-util"
import { Location } from "../location"
2026-06-26 17:55:04 +00:00
import { LocationMutation } from "../location-mutation"
2026-06-04 03:02:17 +00:00
import { PermissionV2 } from "../permission"
2026-07-02 00:50:10 +00:00
import { SessionInstructions } from "../session/instructions"
2026-06-09 18:28:45 +00:00
import { AbsolutePath } from "../schema"
import { ReadToolFileSystem } from "./read-filesystem"
2026-06-07 00:49:12 +00:00
import { Tool } from "./tool"
2026-06-04 03:02:17 +00:00
export const name = "read"
2026-07-02 00:50:10 +00:00
const FILENAME = "AGENTS.md"
2026-06-05 23:48:34 +00:00
const SUPPORTED_IMAGE_MIMES = new Set ( [ "image/jpeg" , "image/png" , "image/gif" , "image/webp" ] )
2026-06-04 03:02:17 +00:00
const LocationInput = Schema . Struct ( {
2026-06-09 18:28:45 +00:00
path : Schema.String ,
offset : ReadToolFileSystem.PageInput.fields.offset.annotate ( {
2026-06-04 03:02:17 +00:00
description : "The 1-based directory entry or text line offset to start reading from" ,
} ) ,
2026-06-09 18:28:45 +00:00
limit : ReadToolFileSystem.PageInput.fields.limit.annotate ( {
2026-06-04 03:02:17 +00:00
description : "The maximum number of directory entries or text lines to read" ,
} ) ,
} )
2026-06-05 18:35:19 +00:00
const Input = LocationInput
2026-06-09 18:28:45 +00:00
const Output = Schema . Union ( [ FileSystem . Content , ReadToolFileSystem . TextPage , ReadToolFileSystem . ListPage ] )
2026-06-04 03:02:17 +00:00
2026-07-03 13:03:53 +00:00
export const Plugin = {
2026-07-05 19:51:15 +00:00
id : "opencode.tool.read" ,
2026-07-03 13:03:53 +00:00
effect : Effect.fn ( "ReadTool.Plugin" ) ( function * ( ctx : PluginContext ) {
2026-06-09 18:28:45 +00:00
const reader = yield * ReadToolFileSystem . Service
2026-06-26 17:55:04 +00:00
const mutation = yield * LocationMutation . Service
2026-06-07 00:49:12 +00:00
const permission = yield * PermissionV2 . Service
2026-07-02 00:50:10 +00:00
const sessionInstructions = yield * SessionInstructions . Service
const fs = yield * FSUtil . Service
const location = yield * Location . Service
2026-06-04 03:02:17 +00:00
2026-07-03 13:03:53 +00:00
yield * ctx . tool
2026-07-06 17:36:19 +00:00
. transform ( ( draft ) = >
draft . add (
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 paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval." ,
input : Input ,
output : Output ,
2026-07-16 16:16:19 +00:00
structured : Schema.toEncoded ( Output ) ,
// Image base64 reaches the model through content items (normalized generically
// at tool settlement); persisting a second copy in structured would store the
// original unresized bytes in the message row.
toStructuredOutput : ( { output } ) = >
"encoding" in output && output . encoding === "base64" ? { . . . output , content : "" } : output ,
2026-07-06 17:36:19 +00:00
toModelOutput : ( { input , output } ) = > {
if ( ! ( "encoding" in output ) || output . encoding !== "base64" || ! 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 source = {
type : "tool" as const ,
2026-07-14 19:57:16 +00:00
messageID : context.messageID ,
callID : context.callID ,
2026-07-06 17:36:19 +00:00
}
const target = yield * mutation . resolve ( { path : input.path , kind : "directory" } )
const external = target . externalDirectory
if ( external )
yield * permission . assert ( {
. . . LocationMutation . externalDirectoryPermission ( external ) ,
sessionID : context.sessionID ,
agent : context.agent ,
source ,
} )
const resource = target . resource
const absolute = AbsolutePath . make ( target . canonical )
const type = yield * reader . inspect ( absolute )
2026-06-26 17:55:04 +00:00
yield * permission . assert ( {
2026-07-06 17:36:19 +00:00
action : name ,
resources : [ resource ] ,
save : [ "*" ] ,
2026-06-26 17:55:04 +00:00
sessionID : context.sessionID ,
agent : context.agent ,
source ,
} )
2026-07-06 17:36:19 +00:00
const content =
type === "directory"
? yield * reader . list ( absolute , { offset : input.offset , limit : input.limit } )
: yield * reader . read ( absolute , resource , {
offset : input.offset ,
limit : input.limit ,
} )
// After a successful read, discover nearby AGENTS.md walking up to the Location
// root exclusive and inject them as durable synthetic instructions. For a
// directory listing the walk starts at the directory itself (so its own AGENTS.md
// is discovered); for a file it starts at the file's dirname. External reads are
// skipped, and discovery failures never fail the read.
yield * Effect . gen ( function * ( ) {
if ( target . externalDirectory !== undefined ) return
const resolved = yield * fs . resolve ( target . canonical )
const root = yield * fs . resolve ( location . directory )
// up() searches its stop directory, so the Location-root AGENTS.md (already
2026-07-10 17:26:25 +00:00
// supplied by core initial instructions) is dropped by the dirname filter.
2026-07-06 17:36:19 +00:00
const discovered = yield * fs . up ( {
targets : [ FILENAME ] ,
start : type === "directory" ? resolved : dirname ( resolved ) ,
stop : root ,
} )
const candidates = ( yield * Effect . forEach ( discovered , fs . resolve ) ) . filter (
( file ) = > dirname ( file ) !== root ,
)
if ( candidates . length === 0 ) return
yield * sessionInstructions . load ( { sessionID : context.sessionID , paths : candidates } )
} ) . pipe (
Effect . catch ( ( ) = > Effect . void ) ,
Effect . catchDefect ( ( ) = > Effect . void ) ,
)
2026-07-16 16:16:19 +00:00
if ( "encoding" in content && content . encoding === "base64" && ! SUPPORTED_IMAGE_MIMES . has ( content . mime ) )
2026-07-06 17:36:19 +00:00
return yield * Effect . fail ( new ReadToolFileSystem . BinaryFileError ( { resource } ) )
return content
2026-07-03 13:03:53 +00:00
} ) . pipe (
2026-07-06 17:36:19 +00:00
Effect . mapError ( ( error ) = > {
const message =
error instanceof ReadToolFileSystem . BinaryFileError ||
2026-07-16 16:16:19 +00:00
error instanceof ReadToolFileSystem . MediaIngestLimitError
2026-07-06 17:36:19 +00:00
? error . message
: ` Unable to read ${ input . path } `
2026-07-06 22:14:06 +00:00
return new ToolFailure ( { message , error } )
2026-07-06 17:36:19 +00:00
} ) ,
2026-07-03 13:03:53 +00:00
)
2026-07-06 17:36:19 +00:00
} ,
} ) ,
2026-07-13 02:21:07 +00:00
{ codemode : false } ,
2026-07-06 17:36:19 +00:00
) ,
)
2026-06-07 00:49:12 +00:00
. pipe ( Effect . orDie )
2026-06-04 03:02:17 +00:00
} ) ,
2026-07-03 13:03:53 +00:00
}