Class: Rubino::Tools::AskParentTool
- Defined in:
- lib/rubino/tools/ask_parent_tool.rb
Overview
ask_parent — the child->parent escalation channel (the third mechanism of the parent<->subagent comm design). A subagent calls this when it hits a fork it cannot resolve from its sealed prompt (“sqlite or postgres?”).
Answerer is MIXED: the parent answers from its own context if it can, else it escalates to the HUMAN. This tool implements the wire; the escalation itself reuses Run::ApprovalGate verbatim (the SAME blocking cross-thread hand-off the Option-2 background-approval path already uses):
1. The tool finds the child\'s own BackgroundTasks entry (via the
thread-local Rubino.current_subagent_id set by TaskTool around the
child run). No entry ⇒ this run has no parent (top-level / foreground
sync) and the tool refuses gracefully — never hangs.
2. It registers a Run::ApprovalGate on the entry (BackgroundTasks#begin_ask),
flipping the entry to :blocked_on_human so the parent CLI surfaces the
⛔ blocked banner + the persistent "N subagent waiting on you" marker,
and informs the parent loop by pushing a note onto the parent\'s
InputQueue (so the parent MODEL sees the question at its next turn and
MAY answer it — the "parent answers if it can" half; the parent\'s
answer routes back through the SAME gate via /reply or a parent path).
3. blocking:true → the tool BLOCKS on gate.await(timeout: nil) — wait
INDEFINITELY, no auto-default (the owner constraint). The human answers
via /reply <id>, which decides the gate; the answer is the tool result
and enters the child\'s context as the tool message.
blocking:false → the tool returns IMMEDIATELY ("asked, keep working");
the answer is delivered later as a steer note on the child\'s queue
(Loop#inject_steered_input), so the child keeps making progress.
SUSPEND/RESUME (the W1/#54 lesson): on the CLI a background subagent runs on its OWN dedicated Thread — NOT a pooled Puma/Solid-Queue worker. Parking that dedicated thread on the gate (blocking:true) therefore holds only the child's own thread, never a shared pool, so it cannot freeze the REPL the way a parked Puma worker froze the server (W1). This is exactly how the existing Option-2 approval handler parks the child thread today. A full persist-and-resume suspend (free the thread entirely, rehydrate on answer) is only required for the POOLED web path, which is OUT OF SCOPE here and tracked as a follow-up. A stop (/agents <id> –stop) cancels the gate so a blocking ask unwinds at once instead of waiting forever.
Constant Summary collapse
- NONBLOCKING_ACK =
Sentinel head used when a non-blocking ask returns to the child: the child keeps working and the real answer arrives later as a steer note.
"Question sent to your parent. Keep working with your best " + "judgement; the answer will be delivered to you as a note " + "at your next turn if/when it arrives."
- DEFAULT_ASK_TIMEOUT =
Fallback bound (seconds) for a blocking ask when no configuration is reachable (a bare tool in a unit test). The live value comes from tasks.ask_parent_timeout; this matches the approvals wait-timeout default.
900
Instance Attribute Summary
Attributes inherited from Base
#cancel_token, #read_tracker, #stream_chunk
Instance Method Summary collapse
- #call(arguments) ⇒ Object
-
#config_key ⇒ Object
Gated by the same ‘tools.task` delegation key — it is meaningless without the delegation substrate (BackgroundTasks/registry).
- #description ⇒ Object
- #input_schema ⇒ Object
- #name ⇒ Object
- #risk_level ⇒ Object
Methods inherited from Base
#cancellation_requested?, #emit_chunk, #risky?, #to_tool_definition, workspace_root, workspace_roots
Instance Method Details
#call(arguments) ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 86 def call(arguments) question = (arguments["question"] || arguments[:question]).to_s.strip blocking = blocking_arg(arguments) return "Error: question is required" if question.empty? id = Rubino.current_subagent_id entry = id && BackgroundTasks.instance.find(id) unless entry return "Error: ask_parent is only available to a background subagent " + "(no parent to ask). Resolve this from your task instead." end escalate(entry, question, blocking) rescue Rubino::Interrupted # A /agents <id> --stop (or teardown) cancelled the gate while we were # parked. Unwind cleanly: report it as denied/cancelled so the child can # finish rather than hang. BackgroundTasks.instance.end_ask(entry&.id) if defined?(entry) && entry "Your parent question was cancelled (the run is being stopped)." end |
#config_key ⇒ Object
Gated by the same ‘tools.task` delegation key — it is meaningless without the delegation substrate (BackgroundTasks/registry). Disabling delegation disables ask_parent too.
60 61 62 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 60 def config_key "task" end |
#description ⇒ Object
64 65 66 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 64 def description "Ask YOUR PARENT agent a question when you hit a decision you cannot " + "resolve from the task you were given (e.g. a missing preference, an " + "ambiguous requirement, sqlite-vs-postgres). Your parent answers from " + "its own context if it can, otherwise it asks the human. Use " + "blocking:true when you CANNOT proceed without the answer (you will " + "pause until it arrives); blocking:false (default) when you can keep " + "working and fold the answer in later. Only available to subagents." end |
#input_schema ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 68 def input_schema { type: "object", properties: { question: { type: "string", description: "The question for your parent. Be specific and self-contained." }, blocking: { type: "boolean", description: "true = pause until answered (you cannot proceed without it). " + "false (default) = keep working; the answer is delivered later as a note." } }, required: %w[question] } end |
#name ⇒ Object
53 54 55 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 53 def name "ask_parent" end |
#risk_level ⇒ Object
82 83 84 |
# File 'lib/rubino/tools/ask_parent_tool.rb', line 82 def risk_level :low end |