hermes-client 0.0.0 → 0.1.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 +4 -4
- data/.yardopts +11 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +105 -7
- data/lib/hermes-client.rb +3 -8
- data/lib/hermes_agent/client/configuration.rb +98 -0
- data/lib/hermes_agent/client/conversation.rb +134 -0
- data/lib/hermes_agent/client/entities/capabilities.rb +289 -0
- data/lib/hermes_agent/client/entities/chat_completion.rb +370 -0
- data/lib/hermes_agent/client/entities/health.rb +140 -0
- data/lib/hermes_agent/client/entities/job.rb +394 -0
- data/lib/hermes_agent/client/entities/model.rb +68 -0
- data/lib/hermes_agent/client/entities/response.rb +429 -0
- data/lib/hermes_agent/client/entities/run.rb +427 -0
- data/lib/hermes_agent/client/entities/session_headers.rb +78 -0
- data/lib/hermes_agent/client/entity.rb +89 -0
- data/lib/hermes_agent/client/errors.rb +228 -0
- data/lib/hermes_agent/client/resources/capabilities.rb +34 -0
- data/lib/hermes_agent/client/resources/chat.rb +139 -0
- data/lib/hermes_agent/client/resources/health.rb +49 -0
- data/lib/hermes_agent/client/resources/jobs.rb +213 -0
- data/lib/hermes_agent/client/resources/models.rb +38 -0
- data/lib/hermes_agent/client/resources/responses.rb +204 -0
- data/lib/hermes_agent/client/resources/runs.rb +156 -0
- data/lib/hermes_agent/client/stream.rb +166 -0
- data/lib/hermes_agent/client/transport.rb +281 -0
- data/lib/hermes_agent/client/util.rb +56 -0
- data/lib/hermes_agent/client/version.rb +11 -0
- data/lib/hermes_agent/client.rb +137 -0
- metadata +72 -11
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hermes_agent/client/entity"
|
|
4
|
+
|
|
5
|
+
module HermesAgent
|
|
6
|
+
class Client
|
|
7
|
+
module Entities
|
|
8
|
+
##
|
|
9
|
+
# The token usage reported for a {Run} ({Run#usage}). Like the Responses
|
|
10
|
+
# API (and unlike chat completions), runs report
|
|
11
|
+
# `input_tokens`/`output_tokens` rather than
|
|
12
|
+
# `prompt_tokens`/`completion_tokens`.
|
|
13
|
+
#
|
|
14
|
+
class RunUsage < Entity
|
|
15
|
+
##
|
|
16
|
+
# The number of tokens in the input.
|
|
17
|
+
# @return [Integer, nil]
|
|
18
|
+
#
|
|
19
|
+
def input_tokens
|
|
20
|
+
self["input_tokens"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# The number of tokens in the generated output.
|
|
25
|
+
# @return [Integer, nil]
|
|
26
|
+
#
|
|
27
|
+
def output_tokens
|
|
28
|
+
self["output_tokens"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# The total number of tokens used (input plus output).
|
|
33
|
+
# @return [Integer, nil]
|
|
34
|
+
#
|
|
35
|
+
def total_tokens
|
|
36
|
+
self["total_tokens"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# A run from the Runs API (`POST /v1/runs`, `GET /v1/runs/{id}`). Unlike
|
|
42
|
+
# chat/responses a run is server-side asynchronous: `create` returns
|
|
43
|
+
# immediately with a minimal run (only {#run_id} and {#status}), and
|
|
44
|
+
# progress is tracked by polling ({Resources::Runs#get}) or subscribing to
|
|
45
|
+
# the event stream. {#output} and {#usage} are populated only once the run
|
|
46
|
+
# reaches a terminal `completed` status, and are absent before then and on
|
|
47
|
+
# a `cancelled` or `failed` run — so both readers tolerate a missing
|
|
48
|
+
# value. A `failed` run instead carries an {#error} message. Field readers
|
|
49
|
+
# are best-effort; {#to_h} remains the source of truth.
|
|
50
|
+
#
|
|
51
|
+
class Run < Entity
|
|
52
|
+
##
|
|
53
|
+
# The run id, e.g. `"run_…"` (`run_` plus 32 hex characters). Pass it to
|
|
54
|
+
# {Resources::Runs#get} to poll, or to the streaming/stop/approval calls.
|
|
55
|
+
# @return [String, nil]
|
|
56
|
+
#
|
|
57
|
+
def run_id
|
|
58
|
+
self["run_id"]
|
|
59
|
+
end
|
|
60
|
+
alias id run_id
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# The object type, `"hermes.run"`.
|
|
64
|
+
# @return [String, nil]
|
|
65
|
+
#
|
|
66
|
+
def object
|
|
67
|
+
self["object"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# The run status: `"started"` → `"running"` → terminal `"completed"` /
|
|
72
|
+
# `"cancelled"` / `"failed"`, with `"waiting_for_approval"` while a gated
|
|
73
|
+
# tool awaits a response and a transient `"stopping"` after a stop.
|
|
74
|
+
# @return [String, nil]
|
|
75
|
+
#
|
|
76
|
+
def status
|
|
77
|
+
self["status"]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# When the run was created, as a Unix timestamp (seconds, fractional).
|
|
82
|
+
# @return [Float, nil]
|
|
83
|
+
#
|
|
84
|
+
def created_at
|
|
85
|
+
self["created_at"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# When the run was last updated, as a Unix timestamp (seconds,
|
|
90
|
+
# fractional).
|
|
91
|
+
# @return [Float, nil]
|
|
92
|
+
#
|
|
93
|
+
def updated_at
|
|
94
|
+
self["updated_at"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# The session correlation label for the run (defaults to the {#run_id}
|
|
99
|
+
# when none was supplied on create).
|
|
100
|
+
# @return [String, nil]
|
|
101
|
+
#
|
|
102
|
+
def session_id
|
|
103
|
+
self["session_id"]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# The model that produced the run, e.g. `"hermes-test"` (configured
|
|
108
|
+
# server-side).
|
|
109
|
+
# @return [String, nil]
|
|
110
|
+
#
|
|
111
|
+
def model
|
|
112
|
+
self["model"]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# The name of the most recent event emitted on the run's event stream,
|
|
117
|
+
# e.g. `"run.completed"`. Empty at the very start of a run.
|
|
118
|
+
# @return [String, nil]
|
|
119
|
+
#
|
|
120
|
+
def last_event
|
|
121
|
+
self["last_event"]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# The assembled final assistant text, present once the run completes.
|
|
126
|
+
# Absent (nil) before the run is terminal and on a cancelled run.
|
|
127
|
+
# @return [String, nil]
|
|
128
|
+
#
|
|
129
|
+
def output
|
|
130
|
+
self["output"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# The failure message on a `failed` run (the upstream error, e.g. a
|
|
135
|
+
# model/provider error). Present only when {#status} is `"failed"`;
|
|
136
|
+
# `nil` otherwise. A failed run otherwise looks like a cancelled one —
|
|
137
|
+
# {#output} and {#usage} are absent.
|
|
138
|
+
# @return [String, nil]
|
|
139
|
+
#
|
|
140
|
+
def error
|
|
141
|
+
self["error"]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# The token usage, wrapped in a {RunUsage}. Present once the run
|
|
146
|
+
# completes; returns `nil` when the field is absent (before terminal, or
|
|
147
|
+
# on a cancelled run).
|
|
148
|
+
# @return [RunUsage, nil]
|
|
149
|
+
#
|
|
150
|
+
def usage
|
|
151
|
+
raw = self["usage"]
|
|
152
|
+
raw.is_a?(::Hash) ? RunUsage.new(raw) : nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# The acknowledgement returned by stopping a run
|
|
158
|
+
# ({Resources::Runs#stop}): `{run_id, status: "stopping"}`. Stop is
|
|
159
|
+
# cooperative — this ack only confirms the stop was accepted; the run
|
|
160
|
+
# then resolves to a terminal `"cancelled"` status, observable by polling
|
|
161
|
+
# {Resources::Runs#get}. It is deliberately not a full {Run} (it carries
|
|
162
|
+
# no output, usage, or timestamps).
|
|
163
|
+
#
|
|
164
|
+
class RunStop < Entity
|
|
165
|
+
##
|
|
166
|
+
# The id of the run being stopped (`"run_…"`).
|
|
167
|
+
# @return [String, nil]
|
|
168
|
+
#
|
|
169
|
+
def run_id
|
|
170
|
+
self["run_id"]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
##
|
|
174
|
+
# The status acknowledged by the stop, `"stopping"`.
|
|
175
|
+
# @return [String, nil]
|
|
176
|
+
#
|
|
177
|
+
def status
|
|
178
|
+
self["status"]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# One event in a streamed run ({Resources::Runs#stream_events}).
|
|
184
|
+
#
|
|
185
|
+
# Unlike chat and the Responses API, the run events stream uses plain
|
|
186
|
+
# `data:` frames — there is no SSE `event:` line and no `[DONE]` sentinel
|
|
187
|
+
# — so each event carries its type in an `"event"` payload field (read via
|
|
188
|
+
# {#event}) alongside {#run_id} and a {#timestamp}. The stream has no
|
|
189
|
+
# head frame; it begins at the first content event. Which other readers
|
|
190
|
+
# are meaningful depends on {#event}; the rest return `nil`. Observed
|
|
191
|
+
# types: `tool.started`/`tool.completed`, `message.delta`,
|
|
192
|
+
# `reasoning.available`, the terminal `run.completed`/`run.cancelled`/
|
|
193
|
+
# `run.failed` (the last carries an {#error} string), and
|
|
194
|
+
# `approval.request`/`approval.responded`.
|
|
195
|
+
#
|
|
196
|
+
class RunEvent < Entity
|
|
197
|
+
##
|
|
198
|
+
# The terminal lifecycle event of a streamed run: the last event whose
|
|
199
|
+
# {#event} type is a `run.*` frame (`run.completed`, `run.cancelled`, or
|
|
200
|
+
# `run.failed`). Returns `nil` if the stream closed without one (e.g. it
|
|
201
|
+
# was cut short). Used as the aggregated {Stream#result} of
|
|
202
|
+
# {Resources::Runs#stream_events}.
|
|
203
|
+
#
|
|
204
|
+
# @param events [Array<RunEvent>] The streamed events, in order.
|
|
205
|
+
# @return [RunEvent, nil]
|
|
206
|
+
#
|
|
207
|
+
def self.terminal(events)
|
|
208
|
+
events.reverse_each.find { |event| event.event&.start_with?("run.") }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# The event type, e.g. `"message.delta"` or `"run.completed"`.
|
|
213
|
+
# @return [String, nil]
|
|
214
|
+
#
|
|
215
|
+
def event
|
|
216
|
+
self["event"]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
##
|
|
220
|
+
# The id of the run this event belongs to (`"run_…"`).
|
|
221
|
+
# @return [String, nil]
|
|
222
|
+
#
|
|
223
|
+
def run_id
|
|
224
|
+
self["run_id"]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
##
|
|
228
|
+
# When the event was emitted, as a Unix timestamp (seconds, fractional).
|
|
229
|
+
# @return [Float, nil]
|
|
230
|
+
#
|
|
231
|
+
def timestamp
|
|
232
|
+
self["timestamp"]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
##
|
|
236
|
+
# The tool name on a `tool.started` / `tool.completed` event, e.g.
|
|
237
|
+
# `"terminal"`.
|
|
238
|
+
# @return [String, nil]
|
|
239
|
+
#
|
|
240
|
+
def tool
|
|
241
|
+
self["tool"]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# A preview of the tool invocation on a `tool.started` event (e.g. the
|
|
246
|
+
# command to be run).
|
|
247
|
+
# @return [String, nil]
|
|
248
|
+
#
|
|
249
|
+
def preview
|
|
250
|
+
self["preview"]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
##
|
|
254
|
+
# The tool's execution time on a `tool.completed` event, in seconds.
|
|
255
|
+
# @return [Float, nil]
|
|
256
|
+
#
|
|
257
|
+
def duration
|
|
258
|
+
self["duration"]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
##
|
|
262
|
+
# Whether the tool reported an error on a `tool.completed` event. This
|
|
263
|
+
# is the tool *result* signal, not a lifecycle marker: a failed command
|
|
264
|
+
# — or a denied approval — reports `true` yet the run can still complete.
|
|
265
|
+
# Returns `nil` when there is no boolean flag (the field is absent, or
|
|
266
|
+
# the event is a `run.failed` whose `error` is a message string — read
|
|
267
|
+
# that via {#error}).
|
|
268
|
+
# @return [boolean, nil]
|
|
269
|
+
#
|
|
270
|
+
def error?
|
|
271
|
+
value = self["error"]
|
|
272
|
+
value if [true, false].include?(value)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
##
|
|
276
|
+
# The failure message on a `run.failed` event (a string — the upstream
|
|
277
|
+
# error). `nil` on any other event, including a `tool.completed` whose
|
|
278
|
+
# `error` is the boolean result flag (read that via {#error?}). The two
|
|
279
|
+
# readers split the overloaded `error` payload field by type.
|
|
280
|
+
# @return [String, nil]
|
|
281
|
+
#
|
|
282
|
+
def error
|
|
283
|
+
value = self["error"]
|
|
284
|
+
value if value.is_a?(::String)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
##
|
|
288
|
+
# The incremental assistant text on a `message.delta` event.
|
|
289
|
+
# @return [String, nil]
|
|
290
|
+
#
|
|
291
|
+
def delta
|
|
292
|
+
self["delta"]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
##
|
|
296
|
+
# The full reasoning text on a `reasoning.available` event.
|
|
297
|
+
# @return [String, nil]
|
|
298
|
+
#
|
|
299
|
+
def text
|
|
300
|
+
self["text"]
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
# The assembled final assistant text on a `run.completed` event.
|
|
305
|
+
# @return [String, nil]
|
|
306
|
+
#
|
|
307
|
+
def output
|
|
308
|
+
self["output"]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
##
|
|
312
|
+
# The token usage on a `run.completed` event, wrapped in a {RunUsage}.
|
|
313
|
+
# Returns `nil` when the field is absent.
|
|
314
|
+
# @return [RunUsage, nil]
|
|
315
|
+
#
|
|
316
|
+
def usage
|
|
317
|
+
raw = self["usage"]
|
|
318
|
+
raw.is_a?(::Hash) ? RunUsage.new(raw) : nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
##
|
|
322
|
+
# The command awaiting approval on an `approval.request` event.
|
|
323
|
+
# @return [String, nil]
|
|
324
|
+
#
|
|
325
|
+
def command
|
|
326
|
+
self["command"]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
##
|
|
330
|
+
# The matched approval pattern key on an `approval.request` event.
|
|
331
|
+
# @return [String, nil]
|
|
332
|
+
#
|
|
333
|
+
def pattern_key
|
|
334
|
+
self["pattern_key"]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
##
|
|
338
|
+
# All matched approval pattern keys on an `approval.request` event.
|
|
339
|
+
# Returns `nil` when the field is absent.
|
|
340
|
+
# @return [Array<String>, nil]
|
|
341
|
+
#
|
|
342
|
+
def pattern_keys
|
|
343
|
+
self["pattern_keys"]
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
##
|
|
347
|
+
# A human-readable description of the gated command on an
|
|
348
|
+
# `approval.request` event.
|
|
349
|
+
# @return [String, nil]
|
|
350
|
+
#
|
|
351
|
+
def description
|
|
352
|
+
self["description"]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
##
|
|
356
|
+
# The valid approval choices on an `approval.request` event (e.g.
|
|
357
|
+
# `["once", "session", "always", "deny"]`). Returns `nil` when the field
|
|
358
|
+
# is absent.
|
|
359
|
+
# @return [Array<String>, nil]
|
|
360
|
+
#
|
|
361
|
+
def choices
|
|
362
|
+
self["choices"]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
##
|
|
366
|
+
# The choice that resolved an approval on an `approval.responded` event.
|
|
367
|
+
# @return [String, nil]
|
|
368
|
+
#
|
|
369
|
+
def choice
|
|
370
|
+
self["choice"]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
##
|
|
374
|
+
# The count of approvals resolved on an `approval.responded` event.
|
|
375
|
+
# @return [Integer, nil]
|
|
376
|
+
#
|
|
377
|
+
def resolved
|
|
378
|
+
self["resolved"]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
##
|
|
383
|
+
# The acknowledgement returned by responding to a run's pending approval
|
|
384
|
+
# ({Resources::Runs#respond_approval}): `{object:
|
|
385
|
+
# "hermes.run.approval_response", run_id, choice, resolved}`. The run then
|
|
386
|
+
# resumes (the gated tool executes on an approve, or is aborted on a
|
|
387
|
+
# deny — though a denied run still ends `completed`); observe the
|
|
388
|
+
# `approval.responded` and `tool.completed` frames on the event stream, or
|
|
389
|
+
# poll {Resources::Runs#get}, for the outcome.
|
|
390
|
+
#
|
|
391
|
+
class RunApprovalResponse < Entity
|
|
392
|
+
##
|
|
393
|
+
# The object type, `"hermes.run.approval_response"`.
|
|
394
|
+
# @return [String, nil]
|
|
395
|
+
#
|
|
396
|
+
def object
|
|
397
|
+
self["object"]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
##
|
|
401
|
+
# The id of the run whose approval was answered (`"run_…"`).
|
|
402
|
+
# @return [String, nil]
|
|
403
|
+
#
|
|
404
|
+
def run_id
|
|
405
|
+
self["run_id"]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
##
|
|
409
|
+
# The choice that was submitted: `"once"`, `"session"`, `"always"`, or
|
|
410
|
+
# `"deny"`.
|
|
411
|
+
# @return [String, nil]
|
|
412
|
+
#
|
|
413
|
+
def choice
|
|
414
|
+
self["choice"]
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
##
|
|
418
|
+
# The number of pending approvals resolved by the response.
|
|
419
|
+
# @return [Integer, nil]
|
|
420
|
+
#
|
|
421
|
+
def resolved
|
|
422
|
+
self["resolved"]
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hermes_agent/client/entity"
|
|
4
|
+
|
|
5
|
+
module HermesAgent
|
|
6
|
+
class Client
|
|
7
|
+
module Entities
|
|
8
|
+
##
|
|
9
|
+
# Mixin for entities that carry the server's session-continuity headers
|
|
10
|
+
# (`X-Hermes-Session-ID` / `X-Hermes-Session-Key`) alongside their JSON
|
|
11
|
+
# body. Included by the entities returned from endpoints that surface
|
|
12
|
+
# those headers ({ChatCompletion}, {Response}).
|
|
13
|
+
#
|
|
14
|
+
# The session values come from response *headers*, not the JSON body, so
|
|
15
|
+
# they are stored separately from the wrapped payload: {Entity#to_h} and
|
|
16
|
+
# {Entity#[]} continue to reflect only the body. They do, however,
|
|
17
|
+
# participate in equality ({#==} / `#eql?`) and {#hash}, so two entities
|
|
18
|
+
# with the same body but different sessions are not equal.
|
|
19
|
+
#
|
|
20
|
+
module SessionHeaders
|
|
21
|
+
##
|
|
22
|
+
# Wrap a parsed JSON payload, plus the session headers from the response
|
|
23
|
+
# that produced it.
|
|
24
|
+
#
|
|
25
|
+
# @param data [Hash] The parsed response body, with string keys.
|
|
26
|
+
# @param session_id [String, nil] The `X-Hermes-Session-ID` header
|
|
27
|
+
# value, or `nil` if the response did not carry one.
|
|
28
|
+
# @param session_key [String, nil] The `X-Hermes-Session-Key` header
|
|
29
|
+
# value, or `nil` if the response did not carry one.
|
|
30
|
+
#
|
|
31
|
+
# @private
|
|
32
|
+
def initialize(data, session_id: nil, session_key: nil)
|
|
33
|
+
@session_id = session_id
|
|
34
|
+
@session_key = session_key
|
|
35
|
+
super(data)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# The session id from the response's `X-Hermes-Session-ID` header. The
|
|
40
|
+
# server always returns one (generating a fresh id when the request did
|
|
41
|
+
# not supply a session), except where the endpoint omits the header
|
|
42
|
+
# entirely (e.g. retrieving a response by id), in which case it is `nil`.
|
|
43
|
+
# @return [String, nil]
|
|
44
|
+
#
|
|
45
|
+
attr_reader :session_id
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# The session key from the response's `X-Hermes-Session-Key` header.
|
|
49
|
+
# Present only when a session key was supplied on the request;
|
|
50
|
+
# otherwise `nil`.
|
|
51
|
+
# @return [String, nil]
|
|
52
|
+
#
|
|
53
|
+
attr_reader :session_key
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# Whether this entity equals another: same class, equal body payload,
|
|
57
|
+
# and equal session id/key.
|
|
58
|
+
#
|
|
59
|
+
# @param other [Object] The object to compare against.
|
|
60
|
+
# @return [boolean]
|
|
61
|
+
#
|
|
62
|
+
def ==(other)
|
|
63
|
+
super && other.session_id == @session_id && other.session_key == @session_key
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# A hash code consistent with {#==} and `#eql?`, incorporating the
|
|
68
|
+
# session values.
|
|
69
|
+
#
|
|
70
|
+
# @return [Integer]
|
|
71
|
+
#
|
|
72
|
+
def hash
|
|
73
|
+
[super, @session_id, @session_key].hash
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HermesAgent
|
|
4
|
+
class Client
|
|
5
|
+
##
|
|
6
|
+
# Base class for lightweight wrappers around parsed JSON payloads.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses add method readers for the fields we have mapped, but the raw
|
|
9
|
+
# parsed payload is always available as the source of truth: {#to_h}
|
|
10
|
+
# returns the full hash and {#[]} reads an individual key.
|
|
11
|
+
#
|
|
12
|
+
# Entities are immutable value objects: both the entity and its underlying
|
|
13
|
+
# payload are frozen on construction, and equality ({#==} / {#eql?} /
|
|
14
|
+
# {#hash}) is by class and payload, so entities can be compared and used as
|
|
15
|
+
# Hash keys.
|
|
16
|
+
#
|
|
17
|
+
class Entity
|
|
18
|
+
##
|
|
19
|
+
# Wrap a parsed JSON payload. The payload and the entity are frozen.
|
|
20
|
+
#
|
|
21
|
+
# A `nil` `data` is coerced to an empty hash, so an entity built from a
|
|
22
|
+
# missing envelope (e.g. a response that lacks the nested key a resource
|
|
23
|
+
# extracts) degrades to a reader-returns-`nil` entity rather than raising
|
|
24
|
+
# an opaque `NoMethodError` on the first read. This matches the
|
|
25
|
+
# "best-effort readers over a frozen payload" model the readers already
|
|
26
|
+
# use for absent fields. A non-`nil`, non-Hash payload is kept as-is (the
|
|
27
|
+
# streaming path wraps arbitrary parsed JSON, including arrays, in the
|
|
28
|
+
# base entity and reads it back via {#to_h}).
|
|
29
|
+
#
|
|
30
|
+
# @param data [Hash] The parsed response body, with string keys.
|
|
31
|
+
#
|
|
32
|
+
# @private
|
|
33
|
+
def initialize(data)
|
|
34
|
+
@data = (data.nil? ? {} : data).freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Read a raw field by its server-side (string) key.
|
|
40
|
+
#
|
|
41
|
+
# @param key [String] The field name.
|
|
42
|
+
# @return [Object, nil] The raw value, or `nil` if absent.
|
|
43
|
+
#
|
|
44
|
+
def [](key)
|
|
45
|
+
@data[key]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# The full parsed payload (frozen).
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] The raw response body with string keys.
|
|
52
|
+
#
|
|
53
|
+
def to_h
|
|
54
|
+
@data
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Whether this entity equals another: true when `other` is an instance of
|
|
59
|
+
# the same class wrapping equal payload data.
|
|
60
|
+
#
|
|
61
|
+
# @param other [Object] The object to compare against.
|
|
62
|
+
# @return [boolean]
|
|
63
|
+
#
|
|
64
|
+
def ==(other)
|
|
65
|
+
other.instance_of?(self.class) && other.to_h == @data
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# Alias of {#==}, so entities behave consistently as Hash keys (paired
|
|
70
|
+
# with {#hash}).
|
|
71
|
+
#
|
|
72
|
+
# @param other [Object] The object to compare against.
|
|
73
|
+
# @return [boolean]
|
|
74
|
+
#
|
|
75
|
+
def eql?(other)
|
|
76
|
+
self == other
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# A hash code consistent with {#==} and {#eql?}.
|
|
81
|
+
#
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
#
|
|
84
|
+
def hash
|
|
85
|
+
[self.class, @data].hash
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|