Module: Agentilda::Control

Defined in:
lib/agentilda/control.rb

Overview

The side channel between a keypress and a running agent.

claude -p takes no input once it starts, so the only way to reach an agent mid-flight is a file it has been told to poll: each invocation gets a control file of its own, named in its prompt, and a keypress writes a word into every file currently registered. Like everything else about the prompt this is a request — an agent deep in a tool call reads the file at its next step, not instantly — which is why #quit! also arms a deadline the Executor checks, so "quit" is eventually a guarantee too.

Constant Summary collapse

WRAP_UP =

What a keypress writes into a control file. One word, one line, so an agent can act on File.read(path).strip and nothing subtler.

"WRAP_UP"
STOP =
"STOP"
GRACE =

Seconds between #quit! and the harness terminating whatever is still running. Long enough to write files and a handoff note; short enough that q means quit rather than "quit eventually".

60

Class Method Summary collapse

Class Method Details

.overdue?Boolean

Returns whether the grace period after q has run out.

Returns:

  • (Boolean)

    whether the grace period after q has run out



85
86
87
# File 'lib/agentilda/control.rb', line 85

def overdue?
  @mutex.synchronize { !@deadline.nil? && UI.monotonic > @deadline }
end

.quit!void

This method returns an undefined value.

q: #stop!, plus the loop ends after this round, plus a deadline — GRACE seconds from now — after which the Executor aborts whatever is still running rather than waiting on its goodwill.



73
74
75
76
77
78
79
# File 'lib/agentilda/control.rb', line 73

def quit!
  stop!
  @mutex.synchronize {
    @quit = true
    @deadline ||= UI.monotonic + GRACE
  }
end

.quit?Boolean

Returns whether q has been pressed.

Returns:

  • (Boolean)

    whether q has been pressed



82
# File 'lib/agentilda/control.rb', line 82

def quit? = @mutex.synchronize { @quit }

.register(dir, name) ⇒ String

Register a fresh control file for one invocation and hand back its path, to be named in the agent's prompt.

Parameters:

  • dir (String)

    where the file lives (the trace dir — outside the repository for the same reason traces are)

  • name (String)

    something findable: plan ordinal and agent name

Returns:

  • (String)


37
38
39
40
41
42
43
# File 'lib/agentilda/control.rb', line 37

def register(dir, name)
  FileUtils.mkdir_p(dir)
  path = File.join(dir, "control-#{name}-#{Process.pid}-#{format("%04x", rand(0x10000))}")
  File.write(path, "")
  @mutex.synchronize { @files << path }
  path
end

.release(path) ⇒ void

This method returns an undefined value.

Forget a finished invocation's file. The file itself is removed so a crashed run does not leave stale STOPs for the next one to find.

Parameters:

  • path (String)


50
51
52
53
# File 'lib/agentilda/control.rb', line 50

def release(path)
  @mutex.synchronize { @files.delete(path) }
  FileUtils.rm_f(path)
end

.reset!void

This method returns an undefined value.

Back to rest, for the next Runner#call — and for the suite, where one example's q must not quit every example after it.



93
94
95
96
97
98
99
100
# File 'lib/agentilda/control.rb', line 93

def reset!
  @mutex.synchronize {
    @files.each { |f| FileUtils.rm_f(f) }
    @files.clear
    @quit = false
    @deadline = nil
  }
end

.stop!void

This method returns an undefined value.

n: every running agent is asked to write out what it has and end its turn. The loop keeps going — under chaining that is exactly what hands the plan to the next agent.



66
# File 'lib/agentilda/control.rb', line 66

def stop! = broadcast(STOP)

.wrap_up!void

This method returns an undefined value.

w: every running agent is asked to finish the essential remainder as fast as it can. The loop itself keeps going.



59
# File 'lib/agentilda/control.rb', line 59

def wrap_up! = broadcast(WRAP_UP)