Module: Mistri::Console
- Defined in:
- lib/mistri/console.rb
Overview
The management console: the tools an agent gets for managing the workers it spawns. Every tool is a thin wrapper over Session#children and the Child facade, the same functions a host UI calls, so nothing the agent can do is hidden from the user and nothing the user does confuses the agent. Tools are stateless: each call reads the calling session's children at that moment.
agent = Mistri::Agent.new(provider:, tools: [spawn, *Mistri::Console.tools])
Workers are addressed by name or session id, uniformly, in every tool. When two workers share a name, the most recently spawned one answers; ids stay unambiguous.
Constant Summary collapse
- READ_TIMEOUT =
300- POLL =
0.5
Class Method Summary collapse
-
.await(child, timeout, poll, signal = nil) ⇒ Object
The wait is cooperative like everything else: the run's abort ends it promptly, never held hostage to the timeout.
-
.find(session, ref) ⇒ Object
Ids are advertised as unambiguous, so they resolve first: exact, then an 8+ character prefix (what list_agents displays).
- .line(entry) ⇒ Object
- .list_agents ⇒ Object
-
.message_line(message) ⇒ Object
Tool calls ride the message's content as typed blocks, never as a separate key; render both channels from the blocks.
- .read_agent(timeout: READ_TIMEOUT, poll: POLL) ⇒ Object
-
.render(child, tail) ⇒ Object
A compact, readable transcript for a model: one line per entry, text extracted, long values truncated.
- .render_arguments(block) ⇒ Object
- .steer_agent ⇒ Object
- .stop_agent ⇒ Object
- .text_of(content) ⇒ Object
- .tools(read_timeout: READ_TIMEOUT, poll: POLL) ⇒ Object
- .unknown(session, ref) ⇒ Object
Class Method Details
.await(child, timeout, poll, signal = nil) ⇒ Object
The wait is cooperative like everything else: the run's abort ends it promptly, never held hostage to the timeout.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/mistri/console.rb', line 143 def await(child, timeout, poll, signal = nil) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout until child.finished? status = child.status return "The wait was stopped; #{child.name} is still #{status}." if signal&.aborted? if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline return "#{child.name} is still #{status} after #{timeout.round}s. " \ "Read it without wait to see progress, or stop it." end sleep poll end report = child.report "#{child.name} #{child.status}." + (report ? "\n#{report}" : "") end |
.find(session, ref) ⇒ Object
Ids are advertised as unambiguous, so they resolve first: exact, then an 8+ character prefix (what list_agents displays). Names come last, latest spawn winning on duplicates, so a model-chosen name can never shadow another worker's id.
127 128 129 130 131 132 133 |
# File 'lib/mistri/console.rb', line 127 def find(session, ref) children = session.children children.find { |child| child.session_id == ref } || (ref.to_s.length >= 8 && children.find { |child| child.session_id.start_with?(ref) }) || children.reverse_each.find { |child| child.name == ref } end |
.line(entry) ⇒ Object
170 171 172 173 174 175 176 |
# File 'lib/mistri/console.rb', line 170 def line(entry) case entry["type"] when "message" then (entry["message"] || {}) when Child::TERMINAL ["#{entry["status"]}:", entry["report"] || entry["error"]].compact.join(" ")[0, 300] end end |
.list_agents ⇒ Object
26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/mistri/console.rb', line 26 def list_agents Tool.define( "list_agents", "See your workers: who is running, who finished, who was stopped. " \ "Check here before spawning a duplicate." ) do |_args, context| children = context.session.children next "You have no workers." if children.empty? children.map do |child| "#{child.name} (#{child.session_id[0, 8]}): #{child.status}" end.join("\n") end end |
.message_line(message) ⇒ Object
Tool calls ride the message's content as typed blocks, never as a separate key; render both channels from the blocks.
180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/mistri/console.rb', line 180 def () blocks = ["content"].is_a?(Array) ? ["content"] : [] calls = blocks.filter_map do |block| next unless block.is_a?(Hash) && block["type"] == "tool_call" "#{block["name"]}(#{Console.render_arguments(block)})" end text = Console.text_of(["content"]) parts = [text[0, 240], *calls].reject { |part| part.to_s.empty? } "#{["role"]}: #{parts.join(" | ")}" unless parts.empty? end |
.read_agent(timeout: READ_TIMEOUT, poll: POLL) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/mistri/console.rb', line 41 def read_agent(timeout: READ_TIMEOUT, poll: POLL) Tool.define( "read_agent", "Read what a worker has done so far without interrupting it, or pass " \ "wait to block until it finishes and get its report.", schema: lambda { string :agent, "The worker's name or session id", required: true integer :tail, "How many recent entries to read (default 20)" boolean :wait, "Block until the worker finishes, then return its report" } ) do |args, context| child = Console.find(context.session, args["agent"]) next Console.unknown(context.session, args["agent"]) unless child if args["wait"] Console.await(child, timeout, poll, context.signal) else Console.render(child, args.fetch("tail", 20).to_i.clamp(1, 200)) end end end |
.render(child, tail) ⇒ Object
A compact, readable transcript for a model: one line per entry, text extracted, long values truncated. The gem never summarizes; the caller can.
162 163 164 165 166 167 168 |
# File 'lib/mistri/console.rb', line 162 def render(child, tail) entries = child.transcript(tail: tail) return "#{child.name} (#{child.status}): no entries yet." if entries.empty? lines = entries.map { |entry| Console.line(entry) }.compact "#{child.name} (#{child.status}), last #{lines.length} entries:\n#{lines.join("\n")}" end |
.render_arguments(block) ⇒ Object
192 193 194 195 196 197 198 199 200 201 |
# File 'lib/mistri/console.rb', line 192 def render_arguments(block) if block["arguments_error"] details = { "arguments_error" => block["arguments_error"] } details = { "arguments" => block["arguments"] }.merge(details) if block.key?("arguments") return JSON.generate(details)[0, 80] end arguments = block.key?("arguments") ? block["arguments"] : {} JSON.generate(arguments)[0, 80] end |
.steer_agent ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/mistri/console.rb', line 63 def steer_agent Tool.define( "steer_agent", "Redirect or correct a running worker. It sees your message at its " \ "next step; it may finish first. For a full restart, stop it and " \ "spawn again with better instructions.", schema: lambda { string :agent, "The worker's name or session id", required: true string :message, "What the worker should know or do differently", required: true } ) do |args, context| child = Console.find(context.session, args["agent"]) next Console.unknown(context.session, args["agent"]) unless child status = child.status if Child::LIVE.include?(status) child.say(args.fetch("message")) "Queued. #{child.name} sees it at its next step; it may finish first." else "#{child.name} is #{status}, so there is nothing to steer. " \ "Spawn a new worker for follow-up work." end end end |
.stop_agent ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/mistri/console.rb', line 88 def stop_agent Tool.define( "stop_agent", "Stop one worker. Its partial work is kept and its transcript stays " \ "readable. Other workers and your own run continue.", schema: lambda { string :agent, "The worker's name or session id", required: true } ) do |args, context| child = Console.find(context.session, args["agent"]) next Console.unknown(context.session, args["agent"]) unless child status = child.status if status == :stopped child.stop "#{child.name} is already stopped." elsif !Child::LIVE.include?(status) && status != :interrupted "#{child.name} is already #{status}." elsif !child.stop "Stopping needs a lock adapter (Mistri.locks) and none is configured." else stopped = child.status if stopped == :stopped "Cancelled. #{child.name} is marked stopped; ordinary queue delivery " \ "will not run it again." elsif %i[done failed].include?(stopped) "#{child.name} finished as #{stopped} before the stop took effect." else "Stop requested. #{child.name}'s runner receives the request promptly " \ "and stops at a cooperative boundary; partial work stays readable." end end end end |
.text_of(content) ⇒ Object
203 204 205 206 207 |
# File 'lib/mistri/console.rb', line 203 def text_of(content) return content.to_s unless content.is_a?(Array) content.filter_map { |block| block["text"] if block.is_a?(Hash) }.join(" ") end |
.tools(read_timeout: READ_TIMEOUT, poll: POLL) ⇒ Object
22 23 24 |
# File 'lib/mistri/console.rb', line 22 def tools(read_timeout: READ_TIMEOUT, poll: POLL) [list_agents, read_agent(timeout: read_timeout, poll: poll), steer_agent, stop_agent] end |
.unknown(session, ref) ⇒ Object
135 136 137 138 139 |
# File 'lib/mistri/console.rb', line 135 def unknown(session, ref) names = session.children.map(&:name).uniq known = names.empty? ? "you have no workers" : "your workers: #{names.join(", ")}" "No worker matches #{ref.inspect}; #{known}." end |