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

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.message}"
  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

Yields:

  • (content)


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