Class: Rubino::Tools::AnswerChildTool

Inherits:
Base
  • Object
show all
Defined in:
lib/rubino/tools/answer_child_tool.rb

Overview

answer_child — the MODEL-callable answer to a child’s ask_parent (S4). The agent-parent counterpart of the human ‘/reply <id> <answer>`: when a subagent calls ask_parent and it is OWNED by an agent (not the human), the question lands on this parent’s steer_queue as a [subagent-question] note; the parent model reads it at its next turn and answers it with this tool.

SCOPED AT CALL (like steer/probe, the S1 correction): registered for ALL agents and AUTHORIZED by OWNERSHIP at call time. The caller is the thread-local Rubino.current_subagent_id (nil ⇒ the human / top-level agent). The target must be the caller’s OWN DIRECT child (BackgroundTasks.owned_by?) AND it must actually be waiting on an ask (it has an ask_gate). NOT on any strip list.

Mechanism reuse: it wraps BackgroundTasks#deliver_answer verbatim — the SAME ONE answer wire the human /reply path uses (decide the gate + push the

parent answer

steer note + clear the blocked state). No new transport.

An agent-parent that CANNOT answer from its own context does NOT use this tool: it escalates by calling its OWN ask_parent (recursion up the tree).

Instance Attribute Summary

Attributes inherited from Base

#cancel_token, #read_tracker, #stream_chunk

Instance Method Summary collapse

Methods inherited from Base

#cancellation_requested?, #emit_chunk, #risky?, #to_tool_definition, workspace_root, workspace_roots

Instance Method Details

#call(arguments) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/rubino/tools/answer_child_tool.rb', line 64

def call(arguments)
  task_id = (arguments["task_id"] || arguments[:task_id]).to_s.strip
  answer  = (arguments["answer"]  || arguments[:answer]).to_s.strip
  return "Error: answer is required" if answer.empty?

  caller_id = Rubino.current_subagent_id
  registry  = BackgroundTasks.instance

  # Ownership: only a DIRECT child of the caller may be answered.
  return "Error: #{task_id} is not one of your subagents." unless registry.owned_by?(caller_id, task_id)

  # It must actually be waiting on an ask (deliver_answer no-ops without a
  # live ask_gate). Covers a missing/finished/not-blocked child uniformly.
  return "#{task_id} is not waiting on you." unless registry.deliver_answer(task_id, answer)

  "↳ answered #{task_id}: #{Rubino::Util::Output.elide(answer, 80)}\n✓ #{task_id} resumes at its next turn"
end

#config_keyObject

Gated by the same ‘tools.task` delegation key — answering a child is meaningless without the delegation substrate. Disabling delegation disables answer_child too.



32
33
34
# File 'lib/rubino/tools/answer_child_tool.rb', line 32

def config_key
  "task"
end

#descriptionObject



36
37
38
39
40
41
42
43
44
# File 'lib/rubino/tools/answer_child_tool.rb', line 36

def description
  "Answer one of YOUR OWN subagents that asked you a question via " \
    "ask_parent (you will have received it as a [subagent-question] note). " \
    "The answer is delivered into that child's context: it unblocks a child " \
    "that paused for it and folds into a child that kept working. You can " \
    "ONLY answer a subagent you started (your direct child) that is actually " \
    "waiting on you. If you CANNOT answer from your own context, do NOT guess " \
    "— escalate by calling ask_parent yourself."
end

#input_schemaObject



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/rubino/tools/answer_child_tool.rb', line 46

def input_schema
  {
    type: "object",
    properties: {
      task_id: { type: "string", description: "The id (sa_…) of YOUR subagent that asked you." },
      answer: { type: "string",
                description: "Your answer. Be specific and self-contained — it enters the child's context." }
    },
    required: %w[task_id answer]
  }
end

#nameObject



25
26
27
# File 'lib/rubino/tools/answer_child_tool.rb', line 25

def name
  "answer_child"
end

#risk_levelObject

Answering a child is a low-risk, non-destructive hand-off (the child carries its own approval/risk gates for whatever it does with the answer).



60
61
62
# File 'lib/rubino/tools/answer_child_tool.rb', line 60

def risk_level
  :low
end