read.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. export * as ReadTool from "./read"
  2. import { Tool, ToolFailure } from "@opencode-ai/llm"
  3. import { Cause, Effect, Layer, Schema } from "effect"
  4. import { FileSystem } from "../filesystem"
  5. import { NonNegativeInt, PositiveInt } from "../schema"
  6. import { PermissionV2 } from "../permission"
  7. import { ToolOutputStore } from "../tool-output-store"
  8. import { ToolRegistry } from "../tool-registry"
  9. export const name = "read"
  10. const LocationInput = Schema.Struct({
  11. ...FileSystem.ReadInput.fields,
  12. offset: FileSystem.ListPageInput.fields.offset.annotate({
  13. description: "The 1-based directory entry or text line offset to start reading from",
  14. }),
  15. limit: FileSystem.ListPageInput.fields.limit.annotate({
  16. description: "The maximum number of directory entries or text lines to read",
  17. }),
  18. })
  19. const ResourceInput = Schema.Struct({
  20. resource: Schema.String,
  21. offset: NonNegativeInt.pipe(Schema.optional),
  22. limit: PositiveInt.check(Schema.isLessThanOrEqualTo(ToolOutputStore.MAX_READ_BYTES)).pipe(Schema.optional),
  23. })
  24. const Input = Schema.Union([LocationInput, ResourceInput])
  25. const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage, ToolOutputStore.Page])
  26. const definition = Tool.make({
  27. description:
  28. "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.",
  29. parameters: Input,
  30. success: Success,
  31. })
  32. export const layer = Layer.effectDiscard(
  33. Effect.gen(function* () {
  34. const registry = yield* ToolRegistry.Service
  35. const filesystem = yield* FileSystem.Service
  36. const resources = yield* ToolOutputStore.Service
  37. yield* registry.contribute((editor) =>
  38. editor.set(name, {
  39. tool: definition,
  40. execute: ({ parameters, sessionID, assertPermission }) => {
  41. const input = parameters
  42. return Effect.gen(function* () {
  43. if ("resource" in input)
  44. return yield* resources.read({ sessionID, uri: input.resource, offset: input.offset, limit: input.limit })
  45. const resolved = yield* filesystem.resolveReadPath(input)
  46. if (resolved.type === "directory") {
  47. const { offset, limit } = input
  48. const target = resolved.target
  49. yield* assertPermission({ action: name, resources: [target.resource], save: ["*"] })
  50. const final = yield* filesystem.resolveReadPath(input)
  51. if (
  52. final.type !== "directory" ||
  53. final.target.resource !== target.resource ||
  54. final.target.real !== target.real
  55. )
  56. return yield* Effect.die(new Error("Directory changed after permission approval"))
  57. return yield* filesystem.listPageResolved(final.target, { offset, limit })
  58. }
  59. const target = resolved.target
  60. yield* assertPermission({
  61. action: name,
  62. resources: [target.resource],
  63. save: ["*"],
  64. })
  65. const final = yield* filesystem.resolveReadPath(input)
  66. if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
  67. return yield* Effect.die(new Error("File changed after permission approval"))
  68. if (
  69. final.target.size > FileSystem.MAX_READ_BYTES ||
  70. input.offset !== undefined ||
  71. input.limit !== undefined
  72. )
  73. return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
  74. return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
  75. }).pipe(
  76. Effect.catchCause((cause) =>
  77. Effect.fail(
  78. new ToolFailure({
  79. message: `Unable to read ${"resource" in input ? input.resource : input.path}`,
  80. error: Cause.squash(cause),
  81. }),
  82. ),
  83. ),
  84. )
  85. },
  86. }),
  87. )
  88. }),
  89. )
  90. export const locationLayer = layer.pipe(
  91. Layer.provideMerge(ToolRegistry.layer),
  92. Layer.provideMerge(FileSystem.locationLayer),
  93. Layer.provideMerge(PermissionV2.locationLayer),
  94. Layer.provideMerge(ToolOutputStore.defaultLayer),
  95. )