Class: Rubino::Tools::TaskStopTool

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

Overview

Cancels a running background subagent started by ‘task`. The KillShell analogue: flips the child Runner’s CancelToken (the exact mechanism Run::Executor’s stop-watcher uses for top-level runs), which unwinds the child loop cooperatively at its next cancel checkpoint.

Constant Summary collapse

STOPPABLE =

The live statuses a stop applies to. A child parked on a human approval or an ask_parent gate still holds its thread + concurrency slot (BackgroundTasks#live_status?), so it MUST be stoppable — refusing left a blocked child as a zombie holding its slot until the ask-gate timeout (#197). :stopping is excluded: a second stop is honestly “already stopping — nothing to stop”.

%i[running needs_approval blocked_on_human blocked_on_parent].freeze

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



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/rubino/tools/task_stop_tool.rb', line 49

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

  registry = BackgroundTasks.instance
  entry    = registry.find(task_id)
  return "Error: no background subagent with task_id=#{task_id}" unless entry

  return "[#{task_id}] already #{entry.status} — nothing to stop." unless STOPPABLE.include?(entry.status)

  # Mark the stop first so the list/cards immediately show ◌ stopping and
  # the unwind records as :stopped, not failed (#108/#13).
  registry.request_stop(task_id)
  # Flip the runner's CancelToken BEFORE waking any gate, so a child woken
  # from a parked wait observes the flipped token at its very next
  # checkpoint and unwinds immediately.
  entry.runner&.cancel!
  # A child parked on its OWN approval or ask gate is blocked inside the
  # gate's wait; cancel the gates so it wakes (Interrupted → deny/cancel)
  # and unwinds NOW instead of holding its thread + slot until the bound
  # elapses (#197) — exactly what the human /agents <id> --stop path does.
  entry.approval_gate&.cancel!
  entry.ask_gate&.cancel!
  # Stop-cascade (S5a): wake any descendant parked on a blocking ask_parent
  # so the whole subtree unwinds at once (no orphaned blocked grandchild).
  registry.cancel_descendant_ask_gates(task_id)
  "[#{task_id}] stop requested (subagent '#{entry.subagent}'). " \
    "It will unwind at its next checkpoint; check task_result(\"#{task_id}\")."
end

#config_keyObject



14
15
16
# File 'lib/rubino/tools/task_stop_tool.rb', line 14

def config_key
  "task"
end

#descriptionObject



26
27
28
29
30
# File 'lib/rubino/tools/task_stop_tool.rb', line 26

def description
  "Stop a running background subagent started by `task` — including one " \
    "parked on an approval or an ask_parent question. Cancels the " \
    "subagent's nested run; its task_result will then report failed/cancelled."
end

#input_schemaObject



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

def input_schema
  {
    type: "object",
    properties: {
      task_id: {
        type: "string",
        description: "The task id (sa_…) returned by `task`."
      }
    },
    required: %w[task_id]
  }
end

#nameObject



10
11
12
# File 'lib/rubino/tools/task_stop_tool.rb', line 10

def name
  "task_stop"
end

#risk_levelObject



45
46
47
# File 'lib/rubino/tools/task_stop_tool.rb', line 45

def risk_level
  :medium
end