Module: Mistri::Tools
- Defined in:
- lib/mistri/tools.rb,
lib/mistri/tools/edit_file.rb,
lib/mistri/tools/read_file.rb,
lib/mistri/tools/list_files.rb,
lib/mistri/tools/write_file.rb,
lib/mistri/tools/read_memory.rb,
lib/mistri/tools/find_in_file.rb,
lib/mistri/tools/update_memory.rb
Overview
Built-in tools. Tools.files binds the document tools to a workspace, so "file" means whatever the workspace says it means: a database column, a row in a documents table, or an actual file. The names stay read_file and edit_file because those are the tool names models are trained on.
Constant Summary collapse
- ALIASES =
{ "oldText" => "old_string", "old" => "old_string", "search" => "old_string", "newText" => "new_string", "new" => "new_string", "replace" => "new_string", "replaceAll" => "replace_all", "file" => "path", "filename" => "path" }.freeze
- MAX_READ_CHARS =
20_000
Class Method Summary collapse
-
.edit_file(workspace) ⇒ Object
The model-facing shape is flat old_string, new_string, replace_all on purpose: it is the shape frontier models are trained on, and nested edit arrays measurably degrade their calls.
- .files(workspace) ⇒ Object
- .find_in_file(workspace) ⇒ Object
- .find_matches(content, query, context) ⇒ Object
- .list_files(workspace) ⇒ Object
- .memory(store) ⇒ Object
- .numbered_window(content, offset, limit) ⇒ Object
- .read_file(workspace) ⇒ Object
- .read_memory(memory) ⇒ Object
-
.tolerate(args) ⇒ Object
Absorb the drift real models produce: alias keys, stringly booleans, unknown keys dropped by simply never being read.
-
.update_memory(memory) ⇒ Object
Whole-document replace on purpose: the model rewrites memory as one coherent text instead of appending fragments that drift.
- .with_document(workspace, args) {|content| ... } ⇒ Object
- .write_file(workspace) ⇒ Object
Class Method Details
.edit_file(workspace) ⇒ Object
The model-facing shape is flat old_string, new_string, replace_all on purpose: it is the shape frontier models are trained on, and nested edit arrays measurably degrade their calls. Failures come back in band with the closest region and its exact difference, so the model's retry is one shot.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/mistri/tools/edit_file.rb', line 12 def edit_file(workspace) Tool.define("edit_file", "Replace an exact snippet of a document. Copy old_string verbatim from " \ "read_file output including whitespace, without line-number prefixes. " \ "It must match exactly one place; add surrounding lines to make it " \ "unique, or set replace_all to change every occurrence.", eager_input_streaming: true, schema: lambda { string :path, "Document path", required: true string :old_string, "Exact text to replace (whitespace matters)", required: true string :new_string, "Replacement text", required: true boolean :replace_all, "Replace every occurrence instead of exactly one" }) do |args| args = Tools.tolerate(args) with_document(workspace, args) do |content| result = Edit.replace(content, args["old_string"], args["new_string"], replace_all: args["replace_all"] == true) workspace.write(args["path"], result.content) "Replaced #{result.count} occurrence(s) in #{args["path"]}" end rescue EditError => e "edit_file failed: #{e.}" end end |
.files(workspace) ⇒ Object
15 16 17 18 |
# File 'lib/mistri/tools.rb', line 15 def files(workspace) [read_file(workspace), write_file(workspace), edit_file(workspace), find_in_file(workspace), list_files(workspace)] end |
.find_in_file(workspace) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/mistri/tools/find_in_file.rb', line 7 def find_in_file(workspace) Tool.define("find_in_file", "Find text in a document. Returns line-numbered matches with context, " \ "so you can locate a region without reading the whole document.", schema: lambda { string :path, "Document path", required: true string :query, "Text to find (plain substring)", required: true integer :context, "Context lines around each match" }) do |args| with_document(workspace, args) do |content| Tools.find_matches(content, args["query"], (args["context"] || 2).to_i) end end end |
.find_matches(content, query, context) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/mistri/tools/find_in_file.rb', line 22 def find_matches(content, query, context) lines = content.lines hits = lines.each_index.select { |i| lines[i].include?(query) } return "No matches for #{query.inspect}." if hits.empty? blocks = hits.first(20).map do |hit| from = [hit - context, 0].max to = [hit + context, lines.length - 1].min (from..to).map { |n| "#{n + 1}: #{lines[n]}" }.join end notice = hits.length > 20 ? "\n[#{hits.length - 20} more matches not shown]" : "" "#{blocks.join("---\n")}#{notice}" end |
.list_files(workspace) ⇒ Object
7 8 9 10 11 12 13 14 |
# File 'lib/mistri/tools/list_files.rb', line 7 def list_files(workspace) Tool.define("list_files", "List document paths in the workspace, optionally under a prefix.", schema: -> { string :prefix, "Only paths starting with this" }) do |args| paths = workspace.list(args["prefix"]) paths.empty? ? "No documents found." : paths.join("\n") end end |
.memory(store) ⇒ Object
20 21 22 |
# File 'lib/mistri/tools.rb', line 20 def memory(store) [read_memory(store), update_memory(store)] end |
.numbered_window(content, offset, limit) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/mistri/tools/read_file.rb', line 24 def numbered_window(content, offset, limit) lines = content.lines from = [(offset || 1).to_i, 1].max to = limit ? [from + limit.to_i - 1, lines.length].min : lines.length numbered = (from..to).map { |n| "#{n}: #{lines[n - 1]}" }.join windowed = to < lines.length || from > 1 suffix = windowed ? "\n[showing lines #{from}-#{to} of #{lines.length}]" : "" return "#{numbered}#{suffix}" if numbered.length <= MAX_READ_CHARS cut = numbered[0, MAX_READ_CHARS] cut = cut[0..(cut.rindex("\n") || -1)] "#{cut}\n[truncated at #{MAX_READ_CHARS} chars; use offset/limit to read more]" end |
.read_file(workspace) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# File 'lib/mistri/tools/read_file.rb', line 9 def read_file(workspace) Tool.define("read_file", "Read a document with line numbers. Use offset and limit for a window " \ "into a long document.", schema: lambda { string :path, "Document path", required: true integer :offset, "First line to read (1-based)" integer :limit, "How many lines to read" }) do |args| with_document(workspace, args) do |content| Tools.numbered_window(content, args["offset"], args["limit"]) end end end |
.read_memory(memory) ⇒ Object
7 8 9 10 11 12 13 14 |
# File 'lib/mistri/tools/read_memory.rb', line 7 def read_memory(memory) Tool.define("read_memory", "Read the durable memory: knowledge kept across sessions. Check it " \ "before starting work that earlier sessions may have learned about.") do |_args| content = memory.read content.empty? ? "Memory is empty." : content end end |
.tolerate(args) ⇒ Object
Absorb the drift real models produce: alias keys, stringly booleans, unknown keys dropped by simply never being read.
33 34 35 36 37 38 39 40 |
# File 'lib/mistri/tools.rb', line 33 def tolerate(args) normalized = args.to_h { |key, value| [ALIASES.fetch(key.to_s, key.to_s), value] } case normalized["replace_all"] when "true", "1", 1 then normalized["replace_all"] = true when "false", "0", 0, nil then normalized["replace_all"] = false end normalized end |
.update_memory(memory) ⇒ Object
Whole-document replace on purpose: the model rewrites memory as one coherent text instead of appending fragments that drift.
9 10 11 12 13 14 15 16 17 18 19 20 |
# File 'lib/mistri/tools/update_memory.rb', line 9 def update_memory(memory) Tool.define("update_memory", "Replace the durable memory with an updated version. Pass the FULL " \ "text: what you were given plus what you learned, rewritten to stay " \ "short and current.", schema: lambda { string :content, "The complete new memory text", required: true }) do |args| memory.replace(args["content"]) "Memory updated (#{args["content"].to_s.length} chars)." end end |
.with_document(workspace, args) {|content| ... } ⇒ Object
24 25 26 27 28 29 |
# File 'lib/mistri/tools.rb', line 24 def with_document(workspace, args) content = workspace.read(args["path"]) return "No document at #{args["path"].inspect}. Use list_files to see paths." if content.nil? yield content end |
.write_file(workspace) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/mistri/tools/write_file.rb', line 7 def write_file(workspace) Tool.define("write_file", "Create or fully overwrite a document with the given content.", eager_input_streaming: true, schema: lambda { string :path, "Document path", required: true string :content, "The full document content", required: true }) do |args| workspace.write(args["path"], args["content"]) "Wrote #{args["path"]} (#{args["content"].to_s.length} chars)" end end |