| import type { AgentTool } from "@earendil-works/pi-agent-core"; |
| import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises"; |
| import { dirname } from "path"; |
| import { type Static, Type } from "typebox"; |
| import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts"; |
| import { withFileMutationQueue } from "./file-mutation-queue.ts"; |
| import { resolveToCwd } from "./path-utils.ts"; |
| import { writeRenderers } from "./renderers/write.ts"; |
| import { wrapToolDefinition } from "./tool-definition-wrapper.ts"; |
|
|
| const writeSchema = Type.Object({ |
| path: Type.String({ description: "Path to the file to write (relative or absolute)" }), |
| content: Type.String({ description: "Content to write to the file" }), |
| }); |
|
|
| export const writeToolSystemPromptContribution = { |
| snippet: "Create or overwrite files", |
| guidelines: ["Use write only for new files or complete rewrites."], |
| } as const; |
|
|
| export type WriteToolInput = Static<typeof writeSchema>; |
|
|
| |
| |
| |
| |
| export interface WriteOperations { |
| |
| writeFile: (absolutePath: string, content: string) => Promise<void>; |
| |
| mkdir: (dir: string) => Promise<void>; |
| } |
|
|
| const defaultWriteOperations: WriteOperations = { |
| writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), |
| mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}), |
| }; |
|
|
| export interface WriteToolOptions { |
| |
| operations?: WriteOperations; |
| } |
|
|
| export function createWriteToolDefinition( |
| cwd: string, |
| options?: WriteToolOptions, |
| ): ToolDefinition<typeof writeSchema, undefined> { |
| const ops = options?.operations ?? defaultWriteOperations; |
| return { |
| name: "write", |
| label: "write", |
| description: |
| "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.", |
| promptSnippet: writeToolSystemPromptContribution.snippet, |
| promptGuidelines: [...writeToolSystemPromptContribution.guidelines], |
| parameters: writeSchema, |
| constrainedSampling: { type: "json_schema", strict: "prefer" }, |
| async execute( |
| _toolCallId, |
| { path, content }: { path: string; content: string }, |
| signal?: AbortSignal, |
| _onUpdate?, |
| ctx?: ExtensionContext, |
| ) { |
| const absolutePath = resolveToCwd(path, ctx?.cwd || cwd); |
| const dir = dirname(absolutePath); |
| return withFileMutationQueue(absolutePath, async () => { |
| |
| |
| |
| |
| const throwIfAborted = (): void => { |
| if (signal?.aborted) throw new Error("Operation aborted"); |
| }; |
|
|
| throwIfAborted(); |
| |
| await ops.mkdir(dir); |
| throwIfAborted(); |
|
|
| |
| await ops.writeFile(absolutePath, content); |
| throwIfAborted(); |
|
|
| return { |
| content: [{ type: "text", text: `Successfully wrote to ${path}` }], |
| details: undefined, |
| }; |
| }); |
| }, |
| ...writeRenderers, |
| }; |
| } |
|
|
| export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> { |
| return wrapToolDefinition(createWriteToolDefinition(cwd, options)); |
| } |
|
|