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 and the `/reply` answer path, 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, steers/probes/stops a running one, and routes a human /reply back down to a blocked child.
/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
/reply <id> <answer> → answer a child blocked on a human/parent ask
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>, /reply <id>, /stop <id>, steer, probe).
"(subagents reset when rubino restarts)"
Instance Method Summary collapse
-
#auto_resolve_pending ⇒ Object
Auto-open the EXISTING interactive 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>` or `/reply <id>` (the maintainer’s “auto-open the existing dropdown” ask).
- #handle_agents(arguments) ⇒ Object
-
#handle_reply(arguments) ⇒ Object
child->parent ASK_PARENT answer: /reply <id> <answer>.
-
#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.
Methods included from UI::ProbeWaitIndicator
#probe_thinking_finished, #probe_thinking_started
Constructor Details
#initialize(ui:) ⇒ Agents
Returns a new instance of Agents.
39 40 41 |
# File 'lib/rubino/commands/handlers/agents.rb', line 39 def initialize(ui:) @ui = ui end |
Instance Method Details
#auto_resolve_pending ⇒ Object
Auto-open the EXISTING interactive 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>` or `/reply <id>` (the maintainer’s “auto-open the existing dropdown” ask). 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.
Reuses the SAME primitives the manual slash paths use:
:needs_approval → #resolve_agent_approval (the approve/deny/always
prompt, identical to /agents <id>)
:blocked_on_human → #prompt_reply_answer + #deliver_reply (the ◆ ask
takeover, identical to /reply <id> with no inline
answer)
The manual slash commands stay as a fallback; this is just the primary, zero-typing surface. Approval is offered FIRST (a parked tool holds a concurrency slot and a possibly dangerous side effect, so it is the more urgent gate). 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.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rubino/commands/handlers/agents.rb', line 69 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 if (entry = registry.awaiting_human.first) answer = prompt_reply_answer(entry) if answer.to_s.strip.empty? @ui.info("No answer given — #{entry.id} is still waiting.") else deliver_reply(entry, answer) end return true end false end |
#handle_agents(arguments) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/rubino/commands/handlers/agents.rb', line 87 def handle_agents(arguments) args = arguments.to_s.strip if args.empty? show_agents_list return end tokens = args.split(/\s+/) stop = tokens.delete("--stop") ? true : false id = tokens.shift if id.nil? || id.empty? show_agents_list elsif stop stop_agent(id) elsif 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_reply(arguments) ⇒ Object
child->parent ASK_PARENT answer: /reply <id> <answer>. Resolves the child’s ask gate (Run::ApprovalGate#decide) so a BLOCKING ask unwinds with the answer as its tool result, and ALSO pushes the answer onto the child’s steer queue so a NON-BLOCKING ask folds it in at its next turn boundary. Either way the answer PERSISTS in the child’s context. With no inline answer, falls back to an interactive prompt (the ◆ takeover, like the approval menu). Clears the blocked state and unblocks the tree.
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 |
# File 'lib/rubino/commands/handlers/agents.rb', line 133 def handle_reply(arguments) tokens = arguments.to_s.strip.split(/\s+/) id = tokens.shift if id.nil? || id.empty? show_blocked_agents return end # /reply is UNSCOPED: the human is the ultimate supervisor and may answer # ANY blocked node — one waiting on the human (:blocked_on_human) OR one # waiting on its agent-parent (:blocked_on_parent), if the human chooses # to step in. entry = Tools::BackgroundTasks.instance.find(id) if entry.nil? @ui.error("no background subagent with id #{id}. #{RESET_HINT}") return end unless %i[blocked_on_human blocked_on_parent].include?(entry.status) @ui.error("#{id} is not waiting on you.") return end answer = dequote(tokens.join(" ")) answer = prompt_reply_answer(entry) if answer.to_s.strip.empty? if answer.to_s.strip.empty? @ui.info("No answer given — #{id} is still waiting.") return end deliver_reply(entry, answer) 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.
115 116 117 118 119 120 121 122 123 124 |
# File 'lib/rubino/commands/handlers/agents.rb', line 115 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 |