Class: Pikuri::Tool::Write
- Inherits:
-
Pikuri::Tool
- Object
- Pikuri::Tool
- Pikuri::Tool::Write
- Defined in:
- lib/pikuri/tool/write.rb
Overview
The write tool, expressed as a Pikuri::Tool subclass: instantiating Tool::Write.new(workspace: ws, confirmer: c) produces a tool whose #to_ruby_llm_tool wiring is identical to any bundled tool’s, so ruby_llm sees nothing special about it. Same shape as SubAgent and Read — workspace and confirmer are captured by the execute closure at construction.
Policy
Three branches based on the on-disk state of path:
-
*New file* — write, no prompt.
-
*Existing file, identical content* — return an “Error: …” no-op observation before invoking the confirmer; don’t ask the user to approve a write that wouldn’t change the file. Comparison is byte-strict, in BINARY encoding (trailing-newline-only differences trigger the confirm path; encoding tags can’t make equal bytes compare unequal).
-
*Existing file, content differs* — confirm with “OK to overwrite <path>: <old> → <new> bytes?” via Confirmer; on yes, write. On no, return a decline-Error observation.
Why ask-on-overwrite (Edit doesn’t)
Edit’s old_string argument is an implicit read-check: the model can’t write a correct old_string without having read the file, so blast radius is bounded by what the model actually knows about file state. Write has no such check, so a hallucinated 500-line content could clobber unread work. The confirmation prompt guards exactly the gap Edit’s argument shape already covers.
Side effects
Parent directories are created (FileUtils.mkdir_p) before the write — matches the git add lib/new/dir/foo.rb mental model and mirrors opencode’s and pi’s behavior. Edge case: mkdir_p succeeds but the write fails; an empty directory is left behind. Accepted for v1 — users have version control.
No atomic temp-file+rename. Plain File.write, same as opencode and pi. The crash-safety story is “the user has git”.
Constant Summary collapse
- DESCRIPTION =
Description shown to the LLM. Follows the opencode-shape (summary +
Usage:bullets) prescribed by the project’s tool-description convention. Per-parameter constraints live in the parameter descriptions; theUsage:bullets are for “when do I pick this? how does it chain with other tools?”. <<~DESC Write a file to the workspace, creating parent directories as needed. Usage: - Use for new files or full-file rewrites; for partial changes use `edit` instead. - Overwriting an existing file requires user confirmation; identical content is rejected as a no-op error — if you see that error, re-read the file rather than trying again. - Parent directories are created automatically (mkdir -p). - Writes the exact bytes supplied: no trailing-newline normalization, no encoding conversion. - Paths outside the workspace are refused. DESC
Constants inherited from Pikuri::Tool
CALCULATOR, FETCH, WEB_SCRAPE, WEB_SEARCH
Instance Attribute Summary
Attributes inherited from Pikuri::Tool
#description, #execute, #name, #parameters
Class Method Summary collapse
-
.write(workspace:, confirmer:, path:, content:) ⇒ String
Resolve
pathagainstworkspace, apply the three-branch policy (new / identical / differs), and return either a success observation or an “Error: …” observation.
Instance Method Summary collapse
- #initialize(workspace:, confirmer:) ⇒ Write constructor
Methods inherited from Pikuri::Tool
Constructor Details
#initialize(workspace:, confirmer:) ⇒ Write
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/pikuri/tool/write.rb', line 72 def initialize(workspace:, confirmer:) super( name: 'write', description: DESCRIPTION, parameters: Parameters.build { |p| p.required_string :path, 'Path to the file to write. Relative paths ' \ 'resolve against the workspace root, e.g. ' \ '"lib/foo.rb".' p.required_string :content, 'Full contents to write to the file, e.g. ' \ '"class Foo\nend\n".' }, execute: ->(path:, content:) { Write.write(workspace: workspace, confirmer: confirmer, path: path, content: content) } ) end |
Class Method Details
.write(workspace:, confirmer:, path:, content:) ⇒ String
Resolve path against workspace, apply the three-branch policy (new / identical / differs), and return either a success observation or an “Error: …” observation.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/pikuri/tool/write.rb', line 100 def self.write(workspace:, confirmer:, path:, content:) resolved = workspace.resolve_for_write(path) if resolved.exist? return "Error: #{path} is a directory" if resolved.directory? existing = read_for_compare(resolved, path) if existing == content.b return "Error: #{path} already contains exactly this content — " \ 'no write needed. If you intended a change, re-read the ' \ 'file and try again.' end prompt = "OK to overwrite #{path}: #{existing.bytesize} → #{content.bytesize} bytes? (y/n)" return "Error: user declined the write to #{path}." unless confirmer.confirm?(prompt: prompt) write_bytes(resolved, content) "Updated #{path} (#{existing.bytesize} → #{content.bytesize} bytes)" else write_bytes(resolved, content) "Created #{path} (#{content.bytesize} bytes)" end rescue Tool::Workspace::Error, Error => e "Error: #{e.}" rescue Errno::EACCES => e "Error: cannot write #{path}: #{e.}" end |