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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +31 -0
  3. data/CONFORMANCE.md +71 -0
  4. data/LICENSE +202 -0
  5. data/README.md +135 -0
  6. data/lib/arcp/auth/auth_scheme.rb +16 -0
  7. data/lib/arcp/auth/bearer.rb +38 -0
  8. data/lib/arcp/auth.rb +4 -0
  9. data/lib/arcp/client.rb +354 -0
  10. data/lib/arcp/clock.rb +35 -0
  11. data/lib/arcp/credential.rb +65 -0
  12. data/lib/arcp/credential_provisioner.rb +132 -0
  13. data/lib/arcp/envelope.rb +115 -0
  14. data/lib/arcp/errors.rb +154 -0
  15. data/lib/arcp/ids.rb +18 -0
  16. data/lib/arcp/job/accepted.rb +37 -0
  17. data/lib/arcp/job/agent_ref.rb +18 -0
  18. data/lib/arcp/job/cancel.rb +18 -0
  19. data/lib/arcp/job/event.rb +68 -0
  20. data/lib/arcp/job/event_body/delegate.rb +24 -0
  21. data/lib/arcp/job/event_body/log.rb +20 -0
  22. data/lib/arcp/job/event_body/metric.rb +20 -0
  23. data/lib/arcp/job/event_body/progress.rb +27 -0
  24. data/lib/arcp/job/event_body/result_chunk.rb +42 -0
  25. data/lib/arcp/job/event_body/status.rb +25 -0
  26. data/lib/arcp/job/event_body/thought.rb +12 -0
  27. data/lib/arcp/job/event_body/tool_call.rb +16 -0
  28. data/lib/arcp/job/event_body/tool_result.rb +23 -0
  29. data/lib/arcp/job/event_body/trace_span.rb +30 -0
  30. data/lib/arcp/job/handle.rb +16 -0
  31. data/lib/arcp/job/job_error.rb +31 -0
  32. data/lib/arcp/job/result.rb +30 -0
  33. data/lib/arcp/job/submit.rb +32 -0
  34. data/lib/arcp/job/subscribe.rb +22 -0
  35. data/lib/arcp/job/subscribed.rb +14 -0
  36. data/lib/arcp/job/summary.rb +27 -0
  37. data/lib/arcp/job/unsubscribe.rb +10 -0
  38. data/lib/arcp/job.rb +15 -0
  39. data/lib/arcp/lease.rb +212 -0
  40. data/lib/arcp/message_types.rb +35 -0
  41. data/lib/arcp/runtime/credential_registry.rb +67 -0
  42. data/lib/arcp/runtime/event_log.rb +62 -0
  43. data/lib/arcp/runtime/job_context.rb +167 -0
  44. data/lib/arcp/runtime/job_manager.rb +256 -0
  45. data/lib/arcp/runtime/lease_manager.rb +88 -0
  46. data/lib/arcp/runtime/runtime.rb +125 -0
  47. data/lib/arcp/runtime/session_actor.rb +300 -0
  48. data/lib/arcp/runtime/subscription_manager.rb +57 -0
  49. data/lib/arcp/runtime.rb +10 -0
  50. data/lib/arcp/serializer.rb +43 -0
  51. data/lib/arcp/session/ack.rb +14 -0
  52. data/lib/arcp/session/agent_inventory.rb +51 -0
  53. data/lib/arcp/session/bye.rb +10 -0
  54. data/lib/arcp/session/capability_set.rb +29 -0
  55. data/lib/arcp/session/feature.rb +25 -0
  56. data/lib/arcp/session/hello.rb +34 -0
  57. data/lib/arcp/session/jobs_response.rb +18 -0
  58. data/lib/arcp/session/list_jobs.rb +19 -0
  59. data/lib/arcp/session/ping.rb +14 -0
  60. data/lib/arcp/session/pong.rb +14 -0
  61. data/lib/arcp/session/session_error.rb +23 -0
  62. data/lib/arcp/session/welcome.rb +38 -0
  63. data/lib/arcp/session.rb +26 -0
  64. data/lib/arcp/trace.rb +51 -0
  65. data/lib/arcp/transport/base.rb +29 -0
  66. data/lib/arcp/transport/memory_transport.rb +54 -0
  67. data/lib/arcp/transport/stdio_transport.rb +56 -0
  68. data/lib/arcp/transport/websocket_transport.rb +47 -0
  69. data/lib/arcp/transport.rb +6 -0
  70. data/lib/arcp/version.rb +7 -0
  71. data/lib/arcp.rb +19 -0
  72. data/sig/arcp/client.rbs +16 -0
  73. data/sig/arcp/credential.rbs +51 -0
  74. data/sig/arcp/envelope.rbs +25 -0
  75. data/sig/arcp/errors.rbs +40 -0
  76. data/sig/arcp/job.rbs +83 -0
  77. data/sig/arcp/lease.rbs +41 -0
  78. data/sig/arcp/runtime.rbs +41 -0
  79. data/sig/arcp/serializer.rbs +8 -0
  80. data/sig/arcp/session.rbs +56 -0
  81. data/sig/arcp/transport.rbs +18 -0
  82. data/sig/arcp.rbs +5 -0
  83. 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
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arcp
4
+ module Session
5
+ Bye = Data.define(:reason) do
6
+ def self.from_h(h) = new(reason: h.transform_keys(&:to_s)['reason'])
7
+ def to_h = { 'reason' => reason }.compact
8
+ end
9
+ end
10
+ 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