arcp 1.0.0
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 +7 -0
- data/CHANGELOG.md +31 -0
- data/CONFORMANCE.md +71 -0
- data/LICENSE +202 -0
- data/README.md +135 -0
- data/lib/arcp/auth/auth_scheme.rb +16 -0
- data/lib/arcp/auth/bearer.rb +38 -0
- data/lib/arcp/auth.rb +4 -0
- data/lib/arcp/client.rb +354 -0
- data/lib/arcp/clock.rb +35 -0
- data/lib/arcp/credential.rb +65 -0
- data/lib/arcp/credential_provisioner.rb +132 -0
- data/lib/arcp/envelope.rb +115 -0
- data/lib/arcp/errors.rb +154 -0
- data/lib/arcp/ids.rb +18 -0
- data/lib/arcp/job/accepted.rb +37 -0
- data/lib/arcp/job/agent_ref.rb +18 -0
- data/lib/arcp/job/cancel.rb +18 -0
- data/lib/arcp/job/event.rb +68 -0
- data/lib/arcp/job/event_body/delegate.rb +24 -0
- data/lib/arcp/job/event_body/log.rb +20 -0
- data/lib/arcp/job/event_body/metric.rb +20 -0
- data/lib/arcp/job/event_body/progress.rb +27 -0
- data/lib/arcp/job/event_body/result_chunk.rb +42 -0
- data/lib/arcp/job/event_body/status.rb +25 -0
- data/lib/arcp/job/event_body/thought.rb +12 -0
- data/lib/arcp/job/event_body/tool_call.rb +16 -0
- data/lib/arcp/job/event_body/tool_result.rb +23 -0
- data/lib/arcp/job/event_body/trace_span.rb +30 -0
- data/lib/arcp/job/handle.rb +16 -0
- data/lib/arcp/job/job_error.rb +31 -0
- data/lib/arcp/job/result.rb +30 -0
- data/lib/arcp/job/submit.rb +32 -0
- data/lib/arcp/job/subscribe.rb +22 -0
- data/lib/arcp/job/subscribed.rb +14 -0
- data/lib/arcp/job/summary.rb +27 -0
- data/lib/arcp/job/unsubscribe.rb +10 -0
- data/lib/arcp/job.rb +15 -0
- data/lib/arcp/lease.rb +212 -0
- data/lib/arcp/message_types.rb +35 -0
- data/lib/arcp/runtime/credential_registry.rb +67 -0
- data/lib/arcp/runtime/event_log.rb +62 -0
- data/lib/arcp/runtime/job_context.rb +167 -0
- data/lib/arcp/runtime/job_manager.rb +256 -0
- data/lib/arcp/runtime/lease_manager.rb +88 -0
- data/lib/arcp/runtime/runtime.rb +125 -0
- data/lib/arcp/runtime/session_actor.rb +300 -0
- data/lib/arcp/runtime/subscription_manager.rb +57 -0
- data/lib/arcp/runtime.rb +10 -0
- data/lib/arcp/serializer.rb +43 -0
- data/lib/arcp/session/ack.rb +14 -0
- data/lib/arcp/session/agent_inventory.rb +51 -0
- data/lib/arcp/session/bye.rb +10 -0
- data/lib/arcp/session/capability_set.rb +29 -0
- data/lib/arcp/session/feature.rb +25 -0
- data/lib/arcp/session/hello.rb +34 -0
- data/lib/arcp/session/jobs_response.rb +18 -0
- data/lib/arcp/session/list_jobs.rb +19 -0
- data/lib/arcp/session/ping.rb +14 -0
- data/lib/arcp/session/pong.rb +14 -0
- data/lib/arcp/session/session_error.rb +23 -0
- data/lib/arcp/session/welcome.rb +38 -0
- data/lib/arcp/session.rb +26 -0
- data/lib/arcp/trace.rb +51 -0
- data/lib/arcp/transport/base.rb +29 -0
- data/lib/arcp/transport/memory_transport.rb +54 -0
- data/lib/arcp/transport/stdio_transport.rb +56 -0
- data/lib/arcp/transport/websocket_transport.rb +47 -0
- data/lib/arcp/transport.rb +6 -0
- data/lib/arcp/version.rb +7 -0
- data/lib/arcp.rb +19 -0
- data/sig/arcp/client.rbs +16 -0
- data/sig/arcp/credential.rbs +51 -0
- data/sig/arcp/envelope.rbs +25 -0
- data/sig/arcp/errors.rbs +40 -0
- data/sig/arcp/job.rbs +83 -0
- data/sig/arcp/lease.rbs +41 -0
- data/sig/arcp/runtime.rbs +41 -0
- data/sig/arcp/serializer.rbs +8 -0
- data/sig/arcp/session.rbs +56 -0
- data/sig/arcp/transport.rbs +18 -0
- data/sig/arcp.rbs +5 -0
- metadata +226 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'async/queue'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Arcp
|
|
8
|
+
module Runtime
|
|
9
|
+
# Per-connection runtime actor. Owns one transport, drives the
|
|
10
|
+
# hello/welcome handshake, dispatches inbound envelopes, and serves
|
|
11
|
+
# as the outbox queue subscriptions fan out into.
|
|
12
|
+
class SessionActor
|
|
13
|
+
attr_reader :session_id, :principal, :outbox
|
|
14
|
+
|
|
15
|
+
def initialize(runtime:, transport:)
|
|
16
|
+
@runtime = runtime
|
|
17
|
+
@transport = transport
|
|
18
|
+
@session_id = nil
|
|
19
|
+
@principal = nil
|
|
20
|
+
@outbox = Async::Queue.new
|
|
21
|
+
@last_processed_seq = 0
|
|
22
|
+
@capabilities = nil
|
|
23
|
+
@resume_token = nil
|
|
24
|
+
@heartbeat_task = nil
|
|
25
|
+
@writer_task = nil
|
|
26
|
+
@closed = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run
|
|
30
|
+
Async do |task|
|
|
31
|
+
envelope = @transport.receive
|
|
32
|
+
return if envelope.nil?
|
|
33
|
+
|
|
34
|
+
handshake(envelope)
|
|
35
|
+
spawn_writer(task)
|
|
36
|
+
spawn_heartbeat(task)
|
|
37
|
+
loop_inbound(task)
|
|
38
|
+
ensure
|
|
39
|
+
close_session
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def send_envelope(envelope)
|
|
44
|
+
@outbox.enqueue(envelope)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def handshake(envelope)
|
|
50
|
+
unless envelope.type == Arcp::MessageTypes::SESSION_HELLO
|
|
51
|
+
raise Arcp::Errors::ProtocolViolation, "expected session.hello, got #{envelope.type}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
hello = Arcp::Session::Hello.from_h(envelope.payload)
|
|
55
|
+
token = hello.auth.is_a?(Hash) ? (hello.auth['token'] || hello.auth[:token]) : nil
|
|
56
|
+
@principal = @runtime.auth_verifier.verify(token)
|
|
57
|
+
if @principal.nil?
|
|
58
|
+
send_session_error(envelope.session_id, code: 'UNAUTHENTICATED', message: 'invalid bearer token')
|
|
59
|
+
raise Arcp::Errors::Unauthenticated, 'invalid bearer token'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@session_id = envelope.session_id
|
|
63
|
+
@capabilities = hello.capabilities.intersect(@runtime.local_capabilities(agents_inventory: true))
|
|
64
|
+
@resume_token = Arcp::Ids.resume_token
|
|
65
|
+
|
|
66
|
+
welcome = Arcp::Session::Welcome.new(
|
|
67
|
+
runtime_name: @runtime.name,
|
|
68
|
+
runtime_version: @runtime.version,
|
|
69
|
+
capabilities: @capabilities,
|
|
70
|
+
heartbeat_interval_sec: @runtime.heartbeat_interval_sec,
|
|
71
|
+
resume_token: @resume_token,
|
|
72
|
+
resume_window_sec: @runtime.resume_window_sec
|
|
73
|
+
)
|
|
74
|
+
out = Arcp::Envelope.build(
|
|
75
|
+
type: Arcp::MessageTypes::SESSION_WELCOME,
|
|
76
|
+
session_id: @session_id,
|
|
77
|
+
payload: welcome.to_h
|
|
78
|
+
)
|
|
79
|
+
@transport.send(out)
|
|
80
|
+
@runtime.register_session(@session_id, self)
|
|
81
|
+
rescue Arcp::Error
|
|
82
|
+
raise
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
send_session_error(envelope&.session_id || Arcp::Ids.session_id,
|
|
85
|
+
code: 'INTERNAL_ERROR', message: e.message)
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def send_session_error(session_id, code:, message:)
|
|
90
|
+
err = Arcp::Session::SessionError.new(code: code, message: message,
|
|
91
|
+
retryable: false, details: {})
|
|
92
|
+
env = Arcp::Envelope.build(
|
|
93
|
+
type: Arcp::MessageTypes::SESSION_ERROR,
|
|
94
|
+
session_id: session_id, payload: err.to_h
|
|
95
|
+
)
|
|
96
|
+
@transport.send(env)
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def spawn_writer(parent)
|
|
102
|
+
@writer_task = parent.async do
|
|
103
|
+
loop do
|
|
104
|
+
env = @outbox.dequeue
|
|
105
|
+
break if env.nil? || env == :__arcp_close__
|
|
106
|
+
|
|
107
|
+
@transport.send(env)
|
|
108
|
+
end
|
|
109
|
+
rescue Async::Stop, IOError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def spawn_heartbeat(parent)
|
|
115
|
+
return unless @capabilities.supports?(Arcp::Session::Feature::HEARTBEAT)
|
|
116
|
+
return if @runtime.heartbeat_interval_sec.nil?
|
|
117
|
+
|
|
118
|
+
@heartbeat_task = parent.async do |t|
|
|
119
|
+
loop do
|
|
120
|
+
t.sleep(@runtime.heartbeat_interval_sec)
|
|
121
|
+
ping = Arcp::Session::Ping.new(nonce: Arcp::Ids.envelope_id,
|
|
122
|
+
sent_at: @runtime.clock.now.iso8601)
|
|
123
|
+
send_envelope(Arcp::Envelope.build(
|
|
124
|
+
type: Arcp::MessageTypes::SESSION_PING,
|
|
125
|
+
session_id: @session_id, payload: ping.to_h
|
|
126
|
+
))
|
|
127
|
+
end
|
|
128
|
+
rescue Async::Stop
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def loop_inbound(_parent)
|
|
134
|
+
loop do
|
|
135
|
+
env = @transport.receive
|
|
136
|
+
break if env.nil?
|
|
137
|
+
|
|
138
|
+
dispatch(env)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def dispatch(env)
|
|
143
|
+
case env.type
|
|
144
|
+
when Arcp::MessageTypes::SESSION_BYE
|
|
145
|
+
close_session
|
|
146
|
+
when Arcp::MessageTypes::SESSION_PING
|
|
147
|
+
ping = Arcp::Session::Ping.from_h(env.payload)
|
|
148
|
+
send_envelope(Arcp::Envelope.build(
|
|
149
|
+
type: Arcp::MessageTypes::SESSION_PONG,
|
|
150
|
+
session_id: @session_id,
|
|
151
|
+
payload: Arcp::Session::Pong.new(ping_nonce: ping.nonce,
|
|
152
|
+
received_at: @runtime.clock.now.iso8601).to_h
|
|
153
|
+
))
|
|
154
|
+
when Arcp::MessageTypes::SESSION_PONG
|
|
155
|
+
nil
|
|
156
|
+
when Arcp::MessageTypes::SESSION_ACK
|
|
157
|
+
ack = Arcp::Session::Ack.from_h(env.payload)
|
|
158
|
+
@runtime.event_log.evict_up_to(@session_id, ack.last_processed_seq)
|
|
159
|
+
@last_processed_seq = ack.last_processed_seq
|
|
160
|
+
when Arcp::MessageTypes::SESSION_LIST_JOBS
|
|
161
|
+
handle_list_jobs(env)
|
|
162
|
+
when Arcp::MessageTypes::JOB_SUBMIT
|
|
163
|
+
handle_submit(env)
|
|
164
|
+
when Arcp::MessageTypes::JOB_CANCEL
|
|
165
|
+
handle_cancel(env)
|
|
166
|
+
when Arcp::MessageTypes::JOB_SUBSCRIBE
|
|
167
|
+
handle_subscribe(env)
|
|
168
|
+
when Arcp::MessageTypes::JOB_UNSUBSCRIBE
|
|
169
|
+
handle_unsubscribe(env)
|
|
170
|
+
end
|
|
171
|
+
# forward-compat: unknown wire types fall through silently.
|
|
172
|
+
rescue Arcp::Error => e
|
|
173
|
+
reply_error(env, e)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_list_jobs(env)
|
|
177
|
+
unless @capabilities.supports?(Arcp::Session::Feature::LIST_JOBS)
|
|
178
|
+
raise Arcp::Errors::ProtocolViolation, 'list_jobs not negotiated'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
req = Arcp::Session::ListJobs.from_h(env.payload)
|
|
182
|
+
response = @runtime.job_manager.list(
|
|
183
|
+
principal_id: @principal.id,
|
|
184
|
+
filter: req.filter, limit: req.limit || 50, cursor: req.cursor
|
|
185
|
+
)
|
|
186
|
+
send_envelope(Arcp::Envelope.build(
|
|
187
|
+
type: Arcp::MessageTypes::SESSION_JOBS,
|
|
188
|
+
session_id: @session_id,
|
|
189
|
+
payload: response.to_h.merge('reply_to' => env.id)
|
|
190
|
+
))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def handle_submit(env)
|
|
194
|
+
submit = Arcp::Job::Submit.from_h(env.payload)
|
|
195
|
+
submit.lease_constraints&.validate!
|
|
196
|
+
result = @runtime.job_manager.submit(
|
|
197
|
+
submit: submit, principal_id: @principal.id,
|
|
198
|
+
session_id: @session_id, session_actor: self
|
|
199
|
+
)
|
|
200
|
+
if result.is_a?(Array)
|
|
201
|
+
job_id, resolved_agent, lease, credentials = result
|
|
202
|
+
accepted = Arcp::Job::Accepted.new(
|
|
203
|
+
job_id: job_id, agent: resolved_agent,
|
|
204
|
+
accepted_at: @runtime.clock.now.iso8601,
|
|
205
|
+
lease: lease,
|
|
206
|
+
credentials: credentials
|
|
207
|
+
)
|
|
208
|
+
else
|
|
209
|
+
job_id = result
|
|
210
|
+
record = @runtime.job_manager.lookup(job_id)
|
|
211
|
+
accepted = Arcp::Job::Accepted.new(
|
|
212
|
+
job_id: job_id, agent: record.agent,
|
|
213
|
+
accepted_at: record.created_at,
|
|
214
|
+
lease: @runtime.lease_manager.get(job_id),
|
|
215
|
+
credentials: nil
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
send_envelope(Arcp::Envelope.build(
|
|
219
|
+
type: Arcp::MessageTypes::JOB_ACCEPTED,
|
|
220
|
+
session_id: @session_id, job_id: accepted.job_id,
|
|
221
|
+
payload: accepted.to_h.merge('reply_to' => env.id)
|
|
222
|
+
))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def handle_cancel(env)
|
|
226
|
+
cancel = Arcp::Job::Cancel.from_h(env.payload)
|
|
227
|
+
@runtime.job_manager.cancel(
|
|
228
|
+
job_id: cancel.job_id, principal_id: @principal.id, reason: cancel.reason
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def handle_subscribe(env)
|
|
233
|
+
unless @capabilities.supports?(Arcp::Session::Feature::SUBSCRIBE)
|
|
234
|
+
raise Arcp::Errors::ProtocolViolation, 'subscribe not negotiated'
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
sub = Arcp::Job::Subscribe.from_h(env.payload)
|
|
238
|
+
@runtime.subscription_manager.attach(sub.job_id, @principal.id, @session_id, @outbox)
|
|
239
|
+
|
|
240
|
+
if sub.history
|
|
241
|
+
replay = @runtime.event_log.replay(@session_id, from_event_seq: sub.from_event_seq)
|
|
242
|
+
replay.each { |e| send_envelope(e) }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
subscribed = Arcp::Job::Subscribed.new(
|
|
246
|
+
job_id: sub.job_id, subscribed_from: sub.from_event_seq || 0
|
|
247
|
+
)
|
|
248
|
+
send_envelope(Arcp::Envelope.build(
|
|
249
|
+
type: Arcp::MessageTypes::JOB_SUBSCRIBED,
|
|
250
|
+
session_id: @session_id, job_id: sub.job_id,
|
|
251
|
+
payload: subscribed.to_h.merge('reply_to' => env.id)
|
|
252
|
+
))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def handle_unsubscribe(env)
|
|
256
|
+
unsub = Arcp::Job::Unsubscribe.from_h(env.payload)
|
|
257
|
+
@runtime.subscription_manager.detach(unsub.job_id, @session_id)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def reply_error(env, error)
|
|
261
|
+
if env&.job_id
|
|
262
|
+
job_err = Arcp::Job::JobError.new(
|
|
263
|
+
job_id: env.job_id, final_status: 'error',
|
|
264
|
+
code: error.code, message: error.message,
|
|
265
|
+
retryable: error.retryable?, details: error.details || {}
|
|
266
|
+
)
|
|
267
|
+
send_envelope(Arcp::Envelope.build(
|
|
268
|
+
type: Arcp::MessageTypes::JOB_ERROR,
|
|
269
|
+
session_id: @session_id, job_id: env.job_id,
|
|
270
|
+
payload: job_err.to_h.merge('reply_to' => env.id)
|
|
271
|
+
))
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
err = Arcp::Session::SessionError.new(
|
|
276
|
+
code: error.code, message: error.message,
|
|
277
|
+
retryable: error.retryable?, details: error.details || {}
|
|
278
|
+
)
|
|
279
|
+
payload = err.to_h
|
|
280
|
+
payload['reply_to'] = env.id if env
|
|
281
|
+
send_envelope(Arcp::Envelope.build(
|
|
282
|
+
type: Arcp::MessageTypes::SESSION_ERROR,
|
|
283
|
+
session_id: @session_id || env&.session_id || '',
|
|
284
|
+
payload: payload
|
|
285
|
+
))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def close_session
|
|
289
|
+
return if @closed
|
|
290
|
+
|
|
291
|
+
@closed = true
|
|
292
|
+
@heartbeat_task&.stop
|
|
293
|
+
@writer_task&.stop
|
|
294
|
+
@outbox.enqueue(:__arcp_close__)
|
|
295
|
+
@transport.close
|
|
296
|
+
@runtime.deregister_session(@session_id) if @session_id
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Runtime
|
|
5
|
+
# Tracks per-job subscribers across sessions. Submitter session is
|
|
6
|
+
# registered first; additional subscribers attach via `job.subscribe`
|
|
7
|
+
# and receive a fan-out of every `job.event` and the terminating
|
|
8
|
+
# `job.result` / `job.error`.
|
|
9
|
+
class SubscriptionManager
|
|
10
|
+
def initialize
|
|
11
|
+
@subs = Hash.new { |h, k| h[k] = [] } # job_id => [[session_id, principal_id, queue], …]
|
|
12
|
+
@owners = {} # job_id => principal_id (submitter)
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register_owner(job_id, principal_id, session_id, queue)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@owners[job_id] = principal_id
|
|
19
|
+
@subs[job_id] << [session_id, principal_id, queue]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def attach(job_id, principal_id, session_id, queue)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
unless @owners[job_id] == principal_id
|
|
26
|
+
raise Arcp::Errors::PermissionDenied.new(
|
|
27
|
+
"principal not authorized to observe #{job_id}",
|
|
28
|
+
details: { 'job_id' => job_id }
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@subs[job_id] << [session_id, principal_id, queue]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def detach(job_id, session_id)
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@subs[job_id].reject! { |s, _, _| s == session_id }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fanout(job_id, envelope)
|
|
43
|
+
targets = @mutex.synchronize { @subs[job_id].dup }
|
|
44
|
+
targets.each { |_s, _p, q| q.enqueue(envelope) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def owner_of(job_id) = @mutex.synchronize { @owners[job_id] }
|
|
48
|
+
|
|
49
|
+
def clear(job_id)
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
@subs.delete(job_id)
|
|
52
|
+
@owners.delete(job_id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/arcp/runtime.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'runtime/runtime'
|
|
4
|
+
require_relative 'runtime/credential_registry'
|
|
5
|
+
require_relative 'runtime/job_manager'
|
|
6
|
+
require_relative 'runtime/lease_manager'
|
|
7
|
+
require_relative 'runtime/subscription_manager'
|
|
8
|
+
require_relative 'runtime/event_log'
|
|
9
|
+
require_relative 'runtime/job_context'
|
|
10
|
+
require_relative 'runtime/session_actor'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Arcp
|
|
6
|
+
module Serializer
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
@backend = :stdlib
|
|
10
|
+
|
|
11
|
+
def backend = @backend
|
|
12
|
+
|
|
13
|
+
def backend=(name)
|
|
14
|
+
case name
|
|
15
|
+
when :stdlib, :oj
|
|
16
|
+
@backend = name
|
|
17
|
+
require 'oj' if name == :oj
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError, "unknown serializer backend: #{name.inspect}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dump(value)
|
|
24
|
+
case @backend
|
|
25
|
+
when :oj
|
|
26
|
+
Oj.dump(value, mode: :compat)
|
|
27
|
+
else
|
|
28
|
+
JSON.generate(value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def load(bytes)
|
|
33
|
+
return nil if bytes.nil? || bytes.empty?
|
|
34
|
+
|
|
35
|
+
case @backend
|
|
36
|
+
when :oj
|
|
37
|
+
Oj.load(bytes, mode: :compat)
|
|
38
|
+
else
|
|
39
|
+
JSON.parse(bytes)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
Ack = Data.define(:last_processed_seq) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(last_processed_seq: h.fetch('last_processed_seq'))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h = { 'last_processed_seq' => last_processed_seq }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
AgentEntry = Data.define(:name, :versions, :default) do
|
|
6
|
+
def self.from_hash(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(
|
|
9
|
+
name: h.fetch('name'),
|
|
10
|
+
versions: Array(h['versions']).map(&:to_s).freeze,
|
|
11
|
+
default: h['default']
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
h = { 'name' => name, 'versions' => versions }
|
|
17
|
+
h['default'] = default if default
|
|
18
|
+
h
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
AgentInventory = Data.define(:entries) do
|
|
23
|
+
include Enumerable
|
|
24
|
+
|
|
25
|
+
def self.from_array(arr)
|
|
26
|
+
new(entries: arr.map { |h| AgentEntry.from_hash(h) }.freeze)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def each(&) = entries.each(&)
|
|
30
|
+
def to_a = entries.map(&:to_h)
|
|
31
|
+
|
|
32
|
+
def find(name) = entries.find { |e| e.name == name }
|
|
33
|
+
def default_for(name) = find(name)&.default
|
|
34
|
+
def versions_for(name) = find(name)&.versions || [].freeze
|
|
35
|
+
def names = entries.map(&:name)
|
|
36
|
+
|
|
37
|
+
def resolve(ref)
|
|
38
|
+
name, version = ref.to_s.split('@', 2)
|
|
39
|
+
entry = find(name)
|
|
40
|
+
return nil unless entry
|
|
41
|
+
|
|
42
|
+
version ||= entry.default
|
|
43
|
+
return nil unless version
|
|
44
|
+
|
|
45
|
+
return nil unless entry.versions.empty? || entry.versions.include?(version)
|
|
46
|
+
|
|
47
|
+
"#{name}@#{version}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
CapabilitySet = Data.define(:features, :encodings, :agents) do
|
|
6
|
+
DEFAULT_ENCODINGS = %w[utf8 base64].freeze
|
|
7
|
+
|
|
8
|
+
def self.local(features: Feature::ALL, encodings: DEFAULT_ENCODINGS, agents: nil)
|
|
9
|
+
new(features: features.dup.freeze, encodings: encodings.dup.freeze, agents: agents)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def intersect(other)
|
|
13
|
+
self.class.new(
|
|
14
|
+
features: (features & other.features).freeze,
|
|
15
|
+
encodings: (encodings & other.encodings).freeze,
|
|
16
|
+
agents: other.agents || agents
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def supports?(feature) = features.include?(feature)
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
h = { 'features' => features, 'encodings' => encodings }
|
|
24
|
+
h['agents'] = agents.to_a if agents
|
|
25
|
+
h
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
module Feature
|
|
6
|
+
HEARTBEAT = 'heartbeat'
|
|
7
|
+
ACK = 'ack'
|
|
8
|
+
LIST_JOBS = 'list_jobs'
|
|
9
|
+
SUBSCRIBE = 'subscribe'
|
|
10
|
+
LEASE_EXPIRES_AT = 'lease_expires_at'
|
|
11
|
+
COST_BUDGET = 'cost.budget'
|
|
12
|
+
PROGRESS = 'progress'
|
|
13
|
+
RESULT_CHUNK = 'result_chunk'
|
|
14
|
+
AGENT_VERSIONS = 'agent_versions'
|
|
15
|
+
MODEL_USE = 'model.use'
|
|
16
|
+
PROVISIONED_CREDENTIALS = 'provisioned_credentials'
|
|
17
|
+
|
|
18
|
+
ALL = [
|
|
19
|
+
HEARTBEAT, ACK, LIST_JOBS, SUBSCRIBE, LEASE_EXPIRES_AT,
|
|
20
|
+
COST_BUDGET, PROGRESS, RESULT_CHUNK, AGENT_VERSIONS,
|
|
21
|
+
MODEL_USE, PROVISIONED_CREDENTIALS
|
|
22
|
+
].freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
Hello = Data.define(:client_name, :client_version, :auth, :capabilities, :resume) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
caps = h['capabilities'] || {}
|
|
9
|
+
new(
|
|
10
|
+
client_name: h['client_name'],
|
|
11
|
+
client_version: h['client_version'],
|
|
12
|
+
auth: h['auth'] || {},
|
|
13
|
+
capabilities: CapabilitySet.new(
|
|
14
|
+
features: Array(caps['features']).freeze,
|
|
15
|
+
encodings: Array(caps['encodings']).freeze,
|
|
16
|
+
agents: nil
|
|
17
|
+
),
|
|
18
|
+
resume: h['resume']
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
h = {
|
|
24
|
+
'client_name' => client_name,
|
|
25
|
+
'client_version' => client_version,
|
|
26
|
+
'auth' => auth,
|
|
27
|
+
'capabilities' => capabilities.to_h.except('agents')
|
|
28
|
+
}
|
|
29
|
+
h['resume'] = resume if resume
|
|
30
|
+
h
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
JobsResponse = Data.define(:jobs, :next_cursor) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(jobs: Array(h['jobs']).map(&:freeze).freeze, next_cursor: h['next_cursor'])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
out = { 'jobs' => jobs }
|
|
13
|
+
out['next_cursor'] = next_cursor if next_cursor
|
|
14
|
+
out
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
ListJobs = Data.define(:filter, :limit, :cursor) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(filter: h['filter'] || {}, limit: h['limit'], cursor: h['cursor'])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
out = { 'filter' => filter || {} }
|
|
13
|
+
out['limit'] = limit if limit
|
|
14
|
+
out['cursor'] = cursor if cursor
|
|
15
|
+
out
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
Ping = Data.define(:nonce, :sent_at) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(nonce: h.fetch('nonce'), sent_at: h.fetch('sent_at'))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h = { 'nonce' => nonce, 'sent_at' => sent_at }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
Pong = Data.define(:ping_nonce, :received_at) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(ping_nonce: h.fetch('ping_nonce'), received_at: h.fetch('received_at'))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h = { 'ping_nonce' => ping_nonce, 'received_at' => received_at }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arcp
|
|
4
|
+
module Session
|
|
5
|
+
SessionError = Data.define(:code, :message, :retryable, :details) do
|
|
6
|
+
def self.from_h(h)
|
|
7
|
+
h = h.transform_keys(&:to_s)
|
|
8
|
+
new(
|
|
9
|
+
code: h.fetch('code'),
|
|
10
|
+
message: h['message'],
|
|
11
|
+
retryable: h.fetch('retryable', false),
|
|
12
|
+
details: h['details'] || {}
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
h = { 'code' => code, 'message' => message, 'retryable' => retryable }
|
|
18
|
+
h['details'] = details unless details.nil? || details.empty?
|
|
19
|
+
h
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|