Class: Rubino::Commands::Handlers::Agents
- Inherits:
-
Object
- Object
- Rubino::Commands::Handlers::Agents
- Includes:
- UI::ProbeWaitIndicator
- Defined in:
- lib/rubino/commands/handlers/agents.rb
Overview
The ‘/agents` (alias `/tasks`) drill-in surface, extracted from Commands::Executor (batch B).
The “see what other agents do” surface. Lists background subagents from the BackgroundTasks registry (the async ‘task` substrate), drills into a single one’s result/error, and steers/probes/stops a running one.
/agents → list
/agents <id> → drill-in (result / error / status)
/agents <id> --stop → cancel a running subagent
/agents <id> steer "…" → fire-and-forget note into the child's context
/agents <id> probe "…" → ephemeral read-only peek
Constant Summary collapse
- APPROVAL_ASK_ATTEMPTS =
How many times the parked-child approval prompt re-renders after an empty/aborted read (#144) before giving up and leaving the child parked.
3- RESET_HINT =
Appended to every “no such subagent id” error (item 5). Subagent ids (sa_*) live ONLY in the current process — the BackgroundTasks registry is in-memory, never persisted — so a prior session’s id is genuinely gone after a REPL restart. The bare “no such id” left the user thinking they’d mistyped; this names the real reason so they don’t hunt for a typo. Surfaced from EVERY not-found path (/agents <id>, /stop <id>, steer, probe).
"(subagents reset when rubino restarts)"
Instance Method Summary collapse
-
#auto_resolve_pending ⇒ Object
Auto-open the EXISTING interactive approval prompt for ONE pending subagent request the human must act on — the REPL idle loop calls this at every idle tick so the affordance presents ITSELF instead of forcing the user to guess ‘/agents <id>`.
- #handle_agents(arguments) ⇒ Object
-
#handle_stop_alias(arguments) ⇒ Object
‘/stop <id>` is the discoverable alias for the unguessable `/agents <id> –stop` cancel syntax (FRICTION-4).
-
#initialize(ui:) ⇒ Agents
constructor
A new instance of Agents.
-
#probe_agent(id, question) ⇒ Object
parent->child PROBE: an EPHEMERAL read-only peek.
-
#steer_agent(id, text) ⇒ Object
parent->child STEER: a fire-and-forget note that enters the child’s context at its next turn boundary (Loop#inject_steered_input).
Methods included from UI::ProbeWaitIndicator
#probe_thinking_finished, #probe_thinking_started
Constructor Details
#initialize(ui:) ⇒ Agents
Returns a new instance of Agents.
37 38 39 |
# File 'lib/rubino/commands/handlers/agents.rb', line 37 def initialize(ui:) @ui = ui end |
Instance Method Details
#auto_resolve_pending ⇒ Object
Auto-open the EXISTING interactive approval prompt for ONE pending subagent request the human must act on — the REPL idle loop calls this at every idle tick so the affordance presents ITSELF instead of forcing the user to guess ‘/agents <id>`. A request that arrives mid-turn, or survives a turn that is interrupted/aborted, is re-detected here the next time the REPL returns to idle, so it is never lost. Resolves at most ONE request per call so the loop repaints and re-checks between each. Returns true when it presented a request (the caller re-polls), false when nothing was pending.
SECURITY: this changes WHEN the existing approval prompt appears (now it auto-presents), never WHAT requires approval — the gate semantics, the policy that flips a child to :needs_approval, and the approve/deny/always persistence are untouched. The human still makes the same explicit decision through the same gate.
56 57 58 59 60 61 62 63 |
# File 'lib/rubino/commands/handlers/agents.rb', line 56 def auto_resolve_pending # rubocop:disable Naming/PredicateMethod -- a prompt-presenting mutator that reports whether it surfaced a request, not a pure query registry = Tools::BackgroundTasks.instance if (entry = registry.awaiting_approval.first) resolve_agent_approval(entry) return true end false end |
#handle_agents(arguments) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/rubino/commands/handlers/agents.rb', line 65 def handle_agents(arguments) args = arguments.to_s.strip return show_agents_list if args.empty? tokens = args.split(/\s+/) stop, snapshot, attach = %w[--stop --snapshot --attach].map { |flag| tokens.delete(flag) } id = tokens.shift return show_agents_list if id.nil? || id.empty? return stop_agent(id) if stop # `--attach` is the menu's Enter action: hand the id back to the REPL, # which switches the whole timeline to that agent's (clear + replay) and # scopes the input to it. Internal — not a typed grammar candidate. return { attach_agent: id } if attach return show_agent_detail(id, snapshot: true) if snapshot if tokens.first == "steer" steer_agent(id, dequote(tokens[1..].join(" "))) elsif tokens.first == "probe" probe_agent(id, dequote(tokens[1..].join(" "))) else show_agent_detail(id) end end |
#handle_stop_alias(arguments) ⇒ Object
‘/stop <id>` is the discoverable alias for the unguessable `/agents <id> –stop` cancel syntax (FRICTION-4). A bare `/stop` teaches the syntax and lists running subagents rather than erroring.
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/rubino/commands/handlers/agents.rb', line 93 def handle_stop_alias(arguments) id = arguments.to_s.strip.split(/\s+/).first if id.nil? || id.empty? @ui.info("Stop a running subagent: /stop <id> (same as /agents <id> --stop).") handle_agents("") else handle_agents("#{id} --stop") end :handled end |
#probe_agent(id, question) ⇒ Object
parent->child PROBE: an EPHEMERAL read-only peek. Snapshots the child’s current messages, runs ONE side-inference ([child messages] + question) on the child’s own model, prints the answer in a dashed “ephemeral · not saved” aside, and DISCARDS it — nothing is appended to the child’s history, nothing enters the timeline. The absence of any saved/timeline entry is itself the signal that the peek changed nothing.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/rubino/commands/handlers/agents.rb', line 131 def probe_agent(id, question) if question.to_s.strip.empty? @ui.error(%(usage: /agents #{id} probe "your question")) return end entry = Tools::BackgroundTasks.instance.find(id) unless entry @ui.error("cannot probe #{id} — no such subagent. #{RESET_HINT}") return end @ui.info(pastel.dim("┄┄ probe → #{id} ┄┄ (ephemeral · not saved · child trajectory unchanged)")) # A probe answers from the child's context AT THIS INSTANT; right after # spawn that context is still empty and the child honestly says it isn't # working on anything yet — hint so that doesn't read as broken (#112). if entry.tool_count.to_i.zero? @ui.info(pastel.dim(" (snapshot at this instant — the child just started and its " \ "context is still empty; probe again in a moment)")) end @ui.info("? #{question}") # The peek is a synchronous side-inference (seconds of model wait) with # nothing streaming — show the same thinking row /probe got in #58 so # the gap before the ⟵ answer never looks frozen (#146). TTY only; # Null/API adapters and pipes stay silent. probe_thinking_started(@ui) answer = begin Tools::SubagentProbe.new.peek(entry: entry, question: question) ensure probe_thinking_finished(@ui) end @ui.info("⟵ #{answer}") @ui.info(pastel.dim("┄┄ end probe (nothing was saved to #{id}) ┄┄")) end |
#steer_agent(id, text) ⇒ Object
parent->child STEER: a fire-and-forget note that enters the child’s context at its next turn boundary (Loop#inject_steered_input). Pushes onto the child’s steering queue via BackgroundTasks#steer — the SAME wire the human uses to steer the parent. Echoed with the existing steer vocabulary (▸, “enters child context”) + a card repaint so the parked note shows.
111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/rubino/commands/handlers/agents.rb', line 111 def steer_agent(id, text) if text.to_s.strip.empty? @ui.error(%(usage: /agents #{id} steer "your note")) return end if Tools::BackgroundTasks.instance.steer(id, text) @ui.info("steer ▸ #{id} ← #{truncate(text, 80)} (parked · enters child context next turn)") @ui.set_subagent_cards if @ui.respond_to?(:set_subagent_cards) else @ui.error("cannot steer #{id} — no such running subagent. #{RESET_HINT}") end end |