Class: Mistri::Child

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/child.rb

Overview

A sub-agent as its parent (or the host UI) sees it: a window onto the child's own session. Everything here is derived from the store, so it reads the same from any process, while the child runs and forever after.

session.children            # => [#<Mistri::Child Magpie done>, ...]
child.status                # => :running, :done, :stopped, :failed
child.report                # the terminal entry's report, once finished
child.transcript(tail: 20)  # recent entries, image bytes stripped
child.say("Also check their pricing page")

Status is a walk over the child's own entries: a terminal entry wins; a started child is :running while its lease holds and :interrupted once it lapses (with a lock adapter; without one there is no liveness signal and no-terminal stays :running); a dispatched-but-never-started child is :queued, honestly, because the host's queue owns that gap.

Constant Summary collapse

TERMINAL =
"subagent_result"
DISPATCHED =
"subagent_dispatched"
STARTED =
"subagent_started"
LIVE =

The states a worker can still be caught in: steerable, stoppable, worth waiting on.

%i[running queued].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, session_id:, store:) ⇒ Child

Returns a new instance of Child.



33
34
35
36
37
# File 'lib/mistri/child.rb', line 33

def initialize(name:, session_id:, store:)
  @name = name
  @session_id = session_id
  @store = store
end

Instance Attribute Details

#nameObject (readonly)

Returns the value of attribute name.



27
28
29
# File 'lib/mistri/child.rb', line 27

def name
  @name
end

#session_idObject (readonly)

Returns the value of attribute session_id.



27
28
29
# File 'lib/mistri/child.rb', line 27

def session_id
  @session_id
end

Class Method Details

.lease_key(session_id) ⇒ Object



29
# File 'lib/mistri/child.rb', line 29

def self.lease_key(session_id) = "child:#{session_id}"

.stop_key(session_id) ⇒ Object



31
# File 'lib/mistri/child.rb', line 31

def self.stop_key(session_id) = "child-stop:#{session_id}"

.strip_images(value) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mistri/child.rb', line 107

def self.strip_images(value)
  case value
  when Hash
    if value["data"] && value["mime_type"]
      value.except("data").merge("omitted" => true)
    else
      value.transform_values { |nested| strip_images(nested) }
    end
  when Array then value.map { |nested| strip_images(nested) }
  else value
  end
end

Instance Method Details

#errorObject

The terminal entry's error string, once failed.



65
66
67
# File 'lib/mistri/child.rb', line 65

def error
  terminal_entry&.fetch("error", nil)
end

#finished?Boolean

A terminal entry exists: the child ended as done, stopped, or failed and will never run again. The question a queue retry asks; its inverse (started but no terminal) is what makes a crashed child re-runnable.

Returns:

  • (Boolean)


56
57
58
# File 'lib/mistri/child.rb', line 56

def finished?
  !terminal_entry.nil?
end

#inspectObject



103
104
105
# File 'lib/mistri/child.rb', line 103

def inspect
  "#<Mistri::Child #{name} #{status}>"
end

#reportObject



60
61
62
# File 'lib/mistri/child.rb', line 60

def report
  terminal_entry&.fetch("report", nil)
end

#say(text) ⇒ Object

Queue a message the child folds at its next turn boundary. Delivery is honest, not instant: a child mid-step sees it after that step, and a child that finishes first never sees it.



81
82
83
# File 'lib/mistri/child.rb', line 81

def say(text)
  session.steer(text)
end

#statusObject



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/mistri/child.rb', line 39

def status
  log = session.entries
  terminal = log.reverse_each.find { |entry| entry["type"] == TERMINAL }
  return terminal["status"].to_sym if terminal
  if log.any? { |entry| entry["type"] == DISPATCHED } &&
     log.none? { |entry| entry["type"] == STARTED }
    return :queued
  end
  return :interrupted if Mistri.locks && !Mistri.locks.held?(self.class.lease_key(@session_id))

  :running
end

#stopObject

Ask a live child to stop, from any process. A running child's runner sees the flag within a tick and trips its own signal; a queued child is cancelled outright with a stopped terminal, which the runner honors by never starting it. Stop is cross-process by nature, so it needs a lock adapter; without one this returns false. An action that reports acceptance, not a predicate.



91
92
93
94
95
96
97
# File 'lib/mistri/child.rb', line 91

def stop # rubocop:disable Naming/PredicateMethod
  return false unless Mistri.locks

  session.append(TERMINAL, "status" => "stopped") if status == :queued
  Mistri.locks.set_flag(self.class.stop_key(@session_id))
  true
end

#to_hObject



99
100
101
# File 'lib/mistri/child.rb', line 99

def to_h
  { "name" => name, "session_id" => session_id, "status" => status.to_s }
end

#transcript(tail: 20) ⇒ Object

Recent entries, oldest first, with inline image bytes replaced by a marker: transcripts are for reading and re-sending, not for hauling screenshots back into a context window.



72
73
74
75
76
# File 'lib/mistri/child.rb', line 72

def transcript(tail: 20)
  entries = session.entries
  entries = entries.last(tail) if tail
  entries.map { |entry| self.class.strip_images(entry) }
end