Class: Silas::Schedule

Inherits:
Object
  • Object
show all
Defined in:
lib/silas/schedule.rb,
lib/silas/schedule/compiler.rb

Overview

A schedule is a TRIGGER: it fires on a cron cadence and starts a normal, fully-durable turn. Two forms (eve's shapes):

.md with cron frontmatter — "task mode": the body becomes the turn input,
output discarded (fire-and-forget).
.rb subclassing Silas::Schedule::Handler — programmatic control (fan-out,
continue an existing session, conditional start).

Identity is the filesystem path under app/agent/schedules (subdirs included): schedules/billing/sweep.rb -> "billing/sweep" -> Agent::Schedules::Billing::Sweep. Named agents own their schedules the same way they own tools and skills: app/agents/analyst/schedules/monday_kpis.md -> "agents/analyst/monday_kpis", and its ticks start THAT agent — a staff member's cron never wakes the root agent. Schedules are NOT model-visible capabilities, so they never enter the definitions digest — adding or removing one cannot diverge an in-flight turn.

Defined Under Namespace

Modules: Compiler Classes: Handler

Constant Summary collapse

KINDS =
%i[task handler].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, cron:, queue:, kind:, payload:, agent_name: nil) ⇒ Schedule

Returns a new instance of Schedule.



24
25
26
27
28
29
30
31
# File 'lib/silas/schedule.rb', line 24

def initialize(name:, cron:, queue:, kind:, payload:, agent_name: nil)
  @name = name
  @cron = cron
  @queue = queue
  @kind = kind
  @payload = payload
  @agent_name = agent_name
end

Instance Attribute Details

#agent_nameObject (readonly)

Returns the value of attribute agent_name.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def agent_name
  @agent_name
end

#cronObject (readonly)

Returns the value of attribute cron.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def cron
  @cron
end

#kindObject (readonly)

Returns the value of attribute kind.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def kind
  @kind
end

#nameObject (readonly)

Returns the value of attribute name.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def name
  @name
end

#payloadObject (readonly)

Returns the value of attribute payload.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def payload
  @payload
end

#queueObject (readonly)

Returns the value of attribute queue.



22
23
24
# File 'lib/silas/schedule.rb', line 22

def queue
  @queue
end

Class Method Details

.from_handler(name, klass, agent_name: nil) ⇒ Object

Raises:



83
84
85
86
87
88
89
90
91
92
# File 'lib/silas/schedule.rb', line 83

def self.from_handler(name, klass, agent_name: nil)
  unless klass.ancestors.include?(Schedule::Handler)
    raise Error, "#{klass} (schedule #{name}) must inherit Silas::Schedule::Handler"
  end
  cron = klass.cron
  raise Error, "schedule #{name}: handler must declare `cron` or `every`" if cron.nil?

  new(name: name, cron: cron, queue: (klass.queue || Silas.config.queue_name).to_s,
      kind: :handler, payload: klass, agent_name: agent_name)
end

.from_markdown(name, path, agent_name: nil) ⇒ Object

Raises:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/silas/schedule.rb', line 65

def self.from_markdown(name, path, agent_name: nil)
  content = File.read(path)
  # Tolerate leading whitespace/blank lines (e.g. an ERB comment in a
  # generator template renders to an empty first line).
  if content.sub(/\A\s+/, "") =~ /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
    frontmatter = YAML.safe_load(Regexp.last_match(1)) || {}
    body = Regexp.last_match(2)
  else
    frontmatter = {}
    body = content
  end
  cron = frontmatter["cron"] || frontmatter["schedule"]
  raise Error, "schedule #{name}: missing `cron:` frontmatter" if cron.nil?

  new(name: name, cron: cron.to_s, queue: (frontmatter["queue"] || Silas.config.queue_name).to_s,
      kind: :task, payload: body.strip, agent_name: agent_name)
end

.parse(path, root:) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/silas/schedule.rb', line 33

def self.parse(path, root:)
  agents_dir = root.join("app/agents")
  if path.to_s.start_with?("#{agents_dir}#{File::SEPARATOR}")
    parse_named(path, agents_dir: agents_dir)
  else
    rel = path.relative_path_from(root.join("app/agent/schedules"))
    name = rel.sub_ext("").to_s
    case path.extname
    when ".md" then from_markdown(name, path)
    when ".rb"
      const = "Agent::Schedules::" + name.split("/").map(&:camelize).join("::")
      from_handler(name, const.constantize)
    end
  end
end

.parse_named(path, agents_dir:) ⇒ Object

app/agents//schedules/ — the "agents/" name prefix keeps names (and therefore recurring keys) collision-free against a root schedule that happens to share the path shape.



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/silas/schedule.rb', line 52

def self.parse_named(path, agents_dir:)
  rel = path.relative_path_from(agents_dir)          # analyst/schedules/monday_kpis.md
  agent = rel.each_filename.first
  sched = rel.relative_path_from(Pathname(agent).join("schedules")).sub_ext("").to_s
  name = "agents/#{agent}/#{sched}"
  case path.extname
  when ".md" then from_markdown(name, path, agent_name: agent)
  when ".rb"
    const = "Agents::#{agent.camelize}::Schedules::" + sched.split("/").map(&:camelize).join("::")
    from_handler(name, const.constantize, agent_name: agent)
  end
end

Instance Method Details

#recurring_entryObject



109
110
111
# File 'lib/silas/schedule.rb', line 109

def recurring_entry
  { "class" => "Silas::ScheduleJob", "args" => [ name ], "schedule" => cron, "queue" => queue }
end

#recurring_keyObject



107
# File 'lib/silas/schedule.rb', line 107

def recurring_key = "silas_schedule_#{name.tr('/', '_')}"

#trigger!Object

The only behavioural difference between the two forms. A named agent's task starts THAT agent (its tools, instructions, digest — the loop swaps the scope in for every turn of the session it creates).



97
98
99
100
101
102
103
104
105
# File 'lib/silas/schedule.rb', line 97

def trigger!
  case kind
  when :task
    owner = agent_name ? Silas.agent(agent_name) : Silas.agent
    owner.start(input: payload, metadata: { "trigger" => "schedule", "schedule" => name })
  when :handler
    payload.new(schedule: self).call
  end
end