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.
- .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.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/mistri/console.rb', line 134 def await(child, timeout, poll, signal = nil) deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout while Child::LIVE.include?(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.
118 119 120 121 122 123 124 |
# File 'lib/mistri/console.rb', line 118 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
160 161 162 163 164 165 166 |
# File 'lib/mistri/console.rb', line 160 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.
170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/mistri/console.rb', line 170 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"]}(#{JSON.generate(block["arguments"] || {})[0, 80]})" 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.
152 153 154 155 156 157 158 |
# File 'lib/mistri/console.rb', line 152 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 |
.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 |
# 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 !Child::LIVE.include?(status) "#{child.name} is already #{status}." elsif !child.stop "Stopping needs a lock adapter (Mistri.locks) and none is configured." elsif status == :queued "Cancelled. #{child.name} had not started and never will." else "Stop requested. #{child.name} halts within a second or two; " \ "its partial work stays readable through read_agent." end end end |
.text_of(content) ⇒ Object
182 183 184 185 186 |
# File 'lib/mistri/console.rb', line 182 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
126 127 128 129 130 |
# File 'lib/mistri/console.rb', line 126 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 |