write.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. /**
  2. * Model-facing V2 file-write leaf. Relative paths resolve within the active
  3. * Location. Absolute paths inside that Location are accepted, while explicit
  4. * absolute external paths retain mutation capability through a separate
  5. * external_directory approval before edit approval. Named project references
  6. * are read-oriented and deliberately are not accepted by mutation tools.
  7. */
  8. export * as WriteTool from "./write"
  9. import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
  10. import { Cause, Effect, Layer, Schema } from "effect"
  11. import { FileMutation } from "../file-mutation"
  12. import { LocationMutation } from "../location-mutation"
  13. import { ToolRegistry } from "./registry"
  14. export const name = "write"
  15. // TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
  16. export const Parameters = Schema.Struct({
  17. path: Schema.String.annotate({
  18. description:
  19. "File path to write. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval. Named project references are read-oriented and are not accepted.",
  20. }),
  21. content: Schema.String.annotate({ description: "Content to write to the file" }),
  22. })
  23. export const Success = Schema.Struct({
  24. operation: Schema.Literal("write"),
  25. target: Schema.String,
  26. resource: Schema.String,
  27. existed: Schema.Boolean,
  28. })
  29. export type Success = typeof Success.Type
  30. export const toModelOutput = (output: Success) =>
  31. `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}`
  32. const definition = Tool.make({
  33. description:
  34. "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval. Named project references are read-oriented and are not accepted.",
  35. parameters: Parameters,
  36. success: Success,
  37. toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
  38. })
  39. /** Deferred V2 write UX integrations remain visible at the model-facing seam. */
  40. // TODO: Add formatter integration after V2 formatter runtime exists.
  41. // TODO: Publish watcher/file-edit events after V2 watcher integration exists.
  42. // TODO: Add snapshots / undo after design exists.
  43. // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
  44. export const layer = Layer.effectDiscard(
  45. Effect.gen(function* () {
  46. const registry = yield* ToolRegistry.Service
  47. const mutation = yield* LocationMutation.Service
  48. const files = yield* FileMutation.Service
  49. yield* registry.contribute((editor) =>
  50. editor.set(name, {
  51. tool: definition,
  52. execute: ({ parameters, assertPermission }) =>
  53. Effect.gen(function* () {
  54. const plan = yield* mutation.resolve({ path: parameters.path, kind: "file" })
  55. const external = plan.target.externalDirectory
  56. if (external) yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
  57. yield* assertPermission({ action: "edit", resources: [plan.target.resource], save: ["*"] })
  58. return yield* files.writeTextPreservingBom({ plan, content: parameters.content })
  59. }).pipe(
  60. Effect.catchCause((cause) =>
  61. Effect.fail(
  62. new ToolFailure({ message: `Unable to write ${parameters.path}`, error: Cause.squash(cause) }),
  63. ),
  64. ),
  65. ),
  66. }),
  67. )
  68. }),
  69. )