write.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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.
  6. */
  7. export * as WriteTool from "./write"
  8. import { ToolFailure } from "@opencode-ai/llm"
  9. import { Effect, Layer, Schema } from "effect"
  10. import { FileMutation } from "../file-mutation"
  11. import { LocationMutation } from "../location-mutation"
  12. import { PermissionV2 } from "../permission"
  13. import { Tool } from "./tool"
  14. import { Tools } from "./tools"
  15. export const name = "write"
  16. // TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
  17. export const Input = Schema.Struct({
  18. path: Schema.String.annotate({
  19. description:
  20. "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.",
  21. }),
  22. content: Schema.String.annotate({ description: "Content to write to the file" }),
  23. })
  24. export const Output = Schema.Struct({
  25. operation: Schema.Literal("write"),
  26. target: Schema.String,
  27. resource: Schema.String,
  28. existed: Schema.Boolean,
  29. })
  30. export type Output = typeof Output.Type
  31. export const toModelOutput = (output: Output) =>
  32. `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}`
  33. /** Deferred V2 write UX integrations remain visible at the model-facing seam. */
  34. // TODO: Add formatter integration after V2 formatter runtime exists.
  35. // TODO: Publish watcher/file-edit events after V2 watcher integration exists.
  36. // TODO: Add snapshots / undo after design exists.
  37. // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
  38. export const layer = Layer.effectDiscard(
  39. Effect.gen(function* () {
  40. const tools = yield* Tools.Service
  41. const mutation = yield* LocationMutation.Service
  42. const files = yield* FileMutation.Service
  43. const permission = yield* PermissionV2.Service
  44. yield* tools
  45. .register({
  46. [name]: Tool.withPermission(
  47. Tool.make({
  48. description:
  49. "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.",
  50. input: Input,
  51. output: Output,
  52. toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
  53. execute: (input, context) =>
  54. Effect.gen(function* () {
  55. const source = {
  56. type: "tool" as const,
  57. messageID: context.assistantMessageID,
  58. callID: context.toolCallID,
  59. }
  60. const target = yield* mutation.resolve({ path: input.path, kind: "file" })
  61. const external = target.externalDirectory
  62. if (external)
  63. yield* permission.assert({
  64. ...LocationMutation.externalDirectoryPermission(external),
  65. sessionID: context.sessionID,
  66. agent: context.agent,
  67. source,
  68. })
  69. yield* permission.assert({
  70. action: "edit",
  71. resources: [target.resource],
  72. save: ["*"],
  73. sessionID: context.sessionID,
  74. agent: context.agent,
  75. source,
  76. })
  77. return yield* files.writeTextPreservingBom({ target, content: input.content })
  78. }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to write ${input.path}` }))),
  79. }),
  80. "edit",
  81. ),
  82. })
  83. .pipe(Effect.orDie)
  84. }),
  85. )