| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- /**
- * Model-facing V2 file-write leaf. Relative paths resolve within the active
- * Location. Absolute paths inside that Location are accepted, while explicit
- * absolute external paths retain mutation capability through a separate
- * external_directory approval before edit approval.
- */
- export * as WriteTool from "./write"
- import { ToolFailure } from "@opencode-ai/llm"
- import { Effect, Layer, Schema } from "effect"
- import { FileMutation } from "../file-mutation"
- import { LocationMutation } from "../location-mutation"
- import { PermissionV2 } from "../permission"
- import { Tool } from "./tool"
- import { Tools } from "./tools"
- export const name = "write"
- // TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
- export const Input = Schema.Struct({
- path: Schema.String.annotate({
- description:
- "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.",
- }),
- content: Schema.String.annotate({ description: "Content to write to the file" }),
- })
- export const Output = Schema.Struct({
- operation: Schema.Literal("write"),
- target: Schema.String,
- resource: Schema.String,
- existed: Schema.Boolean,
- })
- export type Output = typeof Output.Type
- export const toModelOutput = (output: Output) =>
- `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}`
- /** Deferred V2 write UX integrations remain visible at the model-facing seam. */
- // TODO: Add formatter integration after V2 formatter runtime exists.
- // TODO: Publish watcher/file-edit events after V2 watcher integration exists.
- // TODO: Add snapshots / undo after design exists.
- // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
- export const layer = Layer.effectDiscard(
- Effect.gen(function* () {
- const tools = yield* Tools.Service
- const mutation = yield* LocationMutation.Service
- const files = yield* FileMutation.Service
- const permission = yield* PermissionV2.Service
- yield* tools
- .register({
- [name]: Tool.withPermission(
- Tool.make({
- description:
- "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.",
- input: Input,
- output: Output,
- toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
- execute: (input, context) =>
- Effect.gen(function* () {
- const source = {
- type: "tool" as const,
- messageID: context.assistantMessageID,
- callID: context.toolCallID,
- }
- const target = yield* mutation.resolve({ path: input.path, kind: "file" })
- const external = target.externalDirectory
- if (external)
- yield* permission.assert({
- ...LocationMutation.externalDirectoryPermission(external),
- sessionID: context.sessionID,
- agent: context.agent,
- source,
- })
- yield* permission.assert({
- action: "edit",
- resources: [target.resource],
- save: ["*"],
- sessionID: context.sessionID,
- agent: context.agent,
- source,
- })
- return yield* files.writeTextPreservingBom({ target, content: input.content })
- }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to write ${input.path}` }))),
- }),
- "edit",
- ),
- })
- .pipe(Effect.orDie)
- }),
- )
|