ask-app-server 0.1.2 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +60 -23
- data/lib/ask/app_server/agent_adapter.rb +239 -57
- data/lib/ask/app_server/cli.rb +31 -16
- data/lib/ask/app_server/connection.rb +73 -0
- data/lib/ask/app_server/event_translator.rb +173 -117
- data/lib/ask/app_server/herdr_reporter.rb +201 -0
- data/lib/ask/app_server/server.rb +333 -185
- data/lib/ask/app_server/session_manager.rb +166 -80
- data/lib/ask/app_server/socket_server.rb +116 -0
- data/lib/ask/app_server/version.rb +1 -1
- data/lib/ask/app_server.rb +2 -0
- data/lib/ask-app-server.rb +4 -0
- metadata +22 -5
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Ask
|
|
7
|
+
module AppServer
|
|
8
|
+
# Pane integration: when the host runs inside a terminal workspace
|
|
9
|
+
# that injects HERDR_SOCKET_PATH and HERDR_PANE_ID into the pane's
|
|
10
|
+
# environment, this reporter connects to the workspace socket and
|
|
11
|
+
# keeps its agent-state sidebar accurate with first-party state —
|
|
12
|
+
# the host KNOWS when a turn is running or an approval is pending,
|
|
13
|
+
# which beats screen scraping:
|
|
14
|
+
#
|
|
15
|
+
# pane.report_agent — working | blocked | idle, on change
|
|
16
|
+
# pane.report_agent_session — the ask session id, for future resume
|
|
17
|
+
# pane.report_metadata — model and session count as sidebar tokens
|
|
18
|
+
#
|
|
19
|
+
# The pane state is the aggregate across the host's sessions (a host
|
|
20
|
+
# can serve many clients): blocked if any session awaits an approval,
|
|
21
|
+
# working if any turn is running, idle otherwise. Events that cannot
|
|
22
|
+
# change the aggregate (model.streaming, tool.*, todos) are ignored.
|
|
23
|
+
#
|
|
24
|
+
# Attach once per session manager: HerdrReporter.attach(manager).
|
|
25
|
+
class HerdrReporter
|
|
26
|
+
SOCKET_PATH_ENV = "HERDR_SOCKET_PATH"
|
|
27
|
+
PANE_ID_ENV = "HERDR_PANE_ID"
|
|
28
|
+
|
|
29
|
+
# Reports originate from the ask host; the workspace uses the pair
|
|
30
|
+
# to dedup and order per-source reports.
|
|
31
|
+
SOURCE = "ask:app-server"
|
|
32
|
+
AGENT = "ask"
|
|
33
|
+
|
|
34
|
+
# Events that can change the aggregate pane state.
|
|
35
|
+
STATE_EVENTS = %w[
|
|
36
|
+
turn.started turn.completed turn.failed turn.aborted
|
|
37
|
+
approval.required approval.updated
|
|
38
|
+
plan.proposed plan.approved plan.rejected
|
|
39
|
+
session.created session.ended
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
# @return [HerdrReporter, nil] a live reporter, or nil when the
|
|
43
|
+
# workspace environment variables are absent
|
|
44
|
+
def self.attach(session_manager, socket_path: ENV[SOCKET_PATH_ENV], pane_id: ENV[PANE_ID_ENV])
|
|
45
|
+
return nil if socket_path.to_s.empty? || pane_id.to_s.empty?
|
|
46
|
+
|
|
47
|
+
new(session_manager: session_manager, socket_path: socket_path, pane_id: pane_id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param session_manager [SessionManager]
|
|
51
|
+
# @param socket_path [String] the workspace socket (HERDR_SOCKET_PATH)
|
|
52
|
+
# @param pane_id [String] this pane (HERDR_PANE_ID)
|
|
53
|
+
def initialize(session_manager:, socket_path:, pane_id:)
|
|
54
|
+
@session_manager = session_manager
|
|
55
|
+
@socket_path = socket_path
|
|
56
|
+
@pane_id = pane_id
|
|
57
|
+
@seq = 0
|
|
58
|
+
@reported_state = nil
|
|
59
|
+
@socket = nil
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@enabled = true
|
|
62
|
+
|
|
63
|
+
session_manager.on_session_event { |session_id, event| on_event(session_id, event) }
|
|
64
|
+
report_state(compute_state)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Stop reporting and drop the workspace connection.
|
|
68
|
+
def close
|
|
69
|
+
@enabled = false
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
@socket&.close rescue nil
|
|
72
|
+
@socket = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def on_event(session_id, event)
|
|
79
|
+
return unless @enabled
|
|
80
|
+
|
|
81
|
+
case event.type
|
|
82
|
+
when "session.created"
|
|
83
|
+
report_session(session_id)
|
|
84
|
+
report_metadata
|
|
85
|
+
end
|
|
86
|
+
return unless STATE_EVENTS.include?(event.type)
|
|
87
|
+
|
|
88
|
+
report_state(compute_state)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Aggregate pane state across all sessions: blocked wins over
|
|
92
|
+
# working wins over idle.
|
|
93
|
+
def compute_state
|
|
94
|
+
blocked = false
|
|
95
|
+
working = false
|
|
96
|
+
@session_manager.store.each do |adapter|
|
|
97
|
+
next unless adapter.session_id
|
|
98
|
+
|
|
99
|
+
if adapter.pending_interactions.any?
|
|
100
|
+
blocked = true
|
|
101
|
+
elsif adapter.running
|
|
102
|
+
working = true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
blocked ? "blocked" : working ? "working" : "idle"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def report_state(state)
|
|
109
|
+
return if state == @reported_state
|
|
110
|
+
|
|
111
|
+
@reported_state = state
|
|
112
|
+
params = {
|
|
113
|
+
pane_id: @pane_id,
|
|
114
|
+
source: SOURCE,
|
|
115
|
+
agent: AGENT,
|
|
116
|
+
state: state,
|
|
117
|
+
seq: next_seq
|
|
118
|
+
}
|
|
119
|
+
params["message"] = "awaiting approval" if state == "blocked"
|
|
120
|
+
send_request("pane.report_agent", params)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def report_session(session_id)
|
|
124
|
+
send_request("pane.report_agent_session", {
|
|
125
|
+
pane_id: @pane_id,
|
|
126
|
+
source: SOURCE,
|
|
127
|
+
agent: AGENT,
|
|
128
|
+
agent_session_id: session_id,
|
|
129
|
+
seq: next_seq
|
|
130
|
+
})
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def report_metadata
|
|
134
|
+
send_request("pane.report_metadata", {
|
|
135
|
+
pane_id: @pane_id,
|
|
136
|
+
source: SOURCE,
|
|
137
|
+
agent: AGENT,
|
|
138
|
+
tokens: {
|
|
139
|
+
"model" => current_model,
|
|
140
|
+
"sessions" => @session_manager.store.count.to_s
|
|
141
|
+
},
|
|
142
|
+
seq: next_seq
|
|
143
|
+
})
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def current_model
|
|
147
|
+
model = nil
|
|
148
|
+
@session_manager.store.each do |adapter|
|
|
149
|
+
model = adapter.instance_variable_get(:@model)
|
|
150
|
+
break if model
|
|
151
|
+
end
|
|
152
|
+
model || ENV.fetch("ASK_APP_SERVER_MODEL", "unknown")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def next_seq
|
|
156
|
+
@mutex.synchronize do
|
|
157
|
+
@seq += 1
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def send_request(method, params)
|
|
162
|
+
socket = ensure_socket
|
|
163
|
+
return unless socket
|
|
164
|
+
|
|
165
|
+
socket.puts(JSON.generate({ id: "#{SOURCE}:#{@seq}", method: method, params: params }))
|
|
166
|
+
socket.flush
|
|
167
|
+
rescue Errno::EPIPE, Errno::ECONNREFUSED, IOError
|
|
168
|
+
# The workspace went away; the next report reconnects
|
|
169
|
+
@socket = nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def ensure_socket
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
return @socket if @socket && !@socket.closed?
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
@socket = UNIXSocket.new(@socket_path)
|
|
178
|
+
@drainer = Thread.new { drain_loop(@socket) }
|
|
179
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED
|
|
180
|
+
@socket = nil
|
|
181
|
+
end
|
|
182
|
+
@socket
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The workspace answers every request; drain responses so the
|
|
187
|
+
# socket never fills. Dies with the socket; the next report
|
|
188
|
+
# reconnects.
|
|
189
|
+
def drain_loop(socket)
|
|
190
|
+
while (line = socket.gets)
|
|
191
|
+
# workspace responses/notifications — nothing to do
|
|
192
|
+
end
|
|
193
|
+
rescue IOError, SystemCallError
|
|
194
|
+
# socket closed
|
|
195
|
+
ensure
|
|
196
|
+
socket.close rescue nil
|
|
197
|
+
@mutex.synchronize { @socket = nil if @socket == socket }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|