Class: Rubino::Tools::SteerTool

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

Overview

steer — the MODEL-callable parent->child steering note (S2). The model counterpart of the human ‘/agents <id> steer “…”` affordance: a parent agent parks a note onto one of ITS OWN running children; the note is folded into that child’s context at its next turn boundary (Loop#inject_steered_input via the child’s steer_queue) and PERSISTS — it changes the child’s trajectory, unlike the ephemeral ‘probe`.

SCOPED AT CALL (the S1 correction): steer is 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?), so a node with no children simply gets a “not your child” error. This tool does NOT touch the human CLI path (executor.rb’s steer_agent stays unscoped) and is NOT on any strip list.

Mechanism reuse: it wraps BackgroundTasks#steer verbatim (the SAME wire the human CLI uses) — no new transport, no new state.

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



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/rubino/tools/steer_tool.rb', line 62

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

  caller_id = Rubino.current_subagent_id
  registry  = BackgroundTasks.instance
  entry     = task_id.empty? ? nil : registry.find(task_id)

  # No such id at all → it is not a steerable running subagent.
  return "Cannot steer #{task_id} — no such running subagent." unless entry
  # Self-steer is meaningless and would loop a note into your own context.
  return "Error: cannot steer yourself." if task_id == caller_id
  # Ownership: only a DIRECT child of the caller may be steered.
  unless registry.owned_by?(caller_id, task_id)
    return "Error: #{task_id} is not one of your subagents — you can only steer children you started."
  end
  # A finished child has no live loop to fold the note into.
  return "Cannot steer #{task_id} — it already finished (#{entry.status})." unless live?(entry.status)

  # Wraps the SAME wire the human CLI uses. A false here means the child's
  # queue vanished between checks (a just-finished child) — treat as gone.
  return "Cannot steer #{task_id} — no such running subagent." unless registry.steer(task_id, note)

  # A child parked on a BLOCKING ask_parent has no next turn until the ask
  # is answered — the note IS queued (deliver-on-unblock), but saying
  # "enters child context next turn" would let the parent believe the
  # redirect took effect (#198). Be honest and point at the one action
  # that unblocks the child.
  if parked_on_ask?(entry)
    return "steer ▸ #{task_id}#{Rubino::Util::Output.elide(note, 80)}  (queued — but #{task_id} is BLOCKED " \
           "on ask_parent and will NOT see it until you answer its question: " \
           "#{Rubino::Util::Output.elide(entry.ask_question, 120)} — unblock it with " \
           "answer_child(task_id: \"#{task_id}\", answer: \"…\"))"
  end

  "steer ▸ #{task_id}#{Rubino::Util::Output.elide(note, 80)}  (parked · enters child context next turn)"
end

#config_keyObject

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



30
31
32
# File 'lib/rubino/tools/steer_tool.rb', line 30

def config_key
  "task"
end

#descriptionObject



34
35
36
37
38
39
40
41
42
# File 'lib/rubino/tools/steer_tool.rb', line 34

def description
  "Steer one of YOUR OWN running subagents: park a short note that is " \
    "folded into that child's context at its NEXT turn (it persists and " \
    "changes what the child does). Use it to course-correct a child you " \
    "started — add a constraint, narrow the scope, flag something it missed. " \
    "You can ONLY steer subagents you started (your direct children); you " \
    "cannot steer yourself, a sibling, or a finished child. The note is " \
    "queued, not delivered instantly — the child sees it between turns."
end

#input_schemaObject



44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rubino/tools/steer_tool.rb', line 44

def input_schema
  {
    type: "object",
    properties: {
      task_id: { type: "string", description: "The id (sa_…) of YOUR running subagent to steer." },
      note: { type: "string",
              description: "The steering note to fold into the child's next turn. Keep it short and self-contained." }
    },
    required: %w[task_id note]
  }
end

#nameObject



23
24
25
# File 'lib/rubino/tools/steer_tool.rb', line 23

def name
  "steer"
end

#risk_levelObject

Steering a child is a low-risk, non-destructive nudge (the child carries its own approval/risk gates for anything it does next).



58
59
60
# File 'lib/rubino/tools/steer_tool.rb', line 58

def risk_level
  :low
end