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.
@@ -0,0 +1,370 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hermes_agent/client/entity"
4
+ require "hermes_agent/client/entities/session_headers"
5
+
6
+ module HermesAgent
7
+ class Client
8
+ module Entities
9
+ ##
10
+ # A single message in a chat completion (the `message` of a
11
+ # {ChatChoice}).
12
+ #
13
+ class ChatMessage < Entity
14
+ ##
15
+ # The role of the message author, e.g. `"assistant"`.
16
+ # @return [String, nil]
17
+ #
18
+ def role
19
+ self["role"]
20
+ end
21
+
22
+ ##
23
+ # The message content. For a plain text completion this is the
24
+ # assistant's reply string; it may be `nil` (e.g. for tool calls).
25
+ # @return [String, nil]
26
+ #
27
+ def content
28
+ self["content"]
29
+ end
30
+ end
31
+
32
+ ##
33
+ # The token usage reported for a chat completion ({ChatCompletion#usage}).
34
+ #
35
+ class ChatUsage < Entity
36
+ ##
37
+ # The number of tokens in the prompt.
38
+ # @return [Integer, nil]
39
+ #
40
+ def prompt_tokens
41
+ self["prompt_tokens"]
42
+ end
43
+
44
+ ##
45
+ # The number of tokens in the generated completion.
46
+ # @return [Integer, nil]
47
+ #
48
+ def completion_tokens
49
+ self["completion_tokens"]
50
+ end
51
+
52
+ ##
53
+ # The total number of tokens used (prompt plus completion).
54
+ # @return [Integer, nil]
55
+ #
56
+ def total_tokens
57
+ self["total_tokens"]
58
+ end
59
+ end
60
+
61
+ ##
62
+ # One choice in a chat completion (one entry of
63
+ # {ChatCompletion#choices}).
64
+ #
65
+ class ChatChoice < Entity
66
+ ##
67
+ # The position of this choice in the list.
68
+ # @return [Integer, nil]
69
+ #
70
+ def index
71
+ self["index"]
72
+ end
73
+
74
+ ##
75
+ # Why generation stopped, e.g. `"stop"`.
76
+ # @return [String, nil]
77
+ #
78
+ def finish_reason
79
+ self["finish_reason"]
80
+ end
81
+
82
+ ##
83
+ # The generated message, wrapped in a {ChatMessage}. Returns `nil`
84
+ # when the field is absent.
85
+ # @return [ChatMessage, nil]
86
+ #
87
+ def message
88
+ raw = self["message"]
89
+ raw.is_a?(::Hash) ? ChatMessage.new(raw) : nil
90
+ end
91
+ end
92
+
93
+ ##
94
+ # One streamed chunk of a chat completion (`object:
95
+ # "chat.completion.chunk"`), as emitted by
96
+ # {Resources::Chat#stream_create}. The convenience readers reflect the
97
+ # first choice (`choices[0]`), which is the common single-choice case;
98
+ # use {#to_h} / {#[]} for multi-choice streams.
99
+ #
100
+ class ChatCompletionChunk < Entity
101
+ ##
102
+ # The completion id (carried on every chunk for a turn).
103
+ # @return [String, nil]
104
+ #
105
+ def id
106
+ self["id"]
107
+ end
108
+
109
+ ##
110
+ # The object type, `"chat.completion.chunk"`.
111
+ # @return [String, nil]
112
+ #
113
+ def object
114
+ self["object"]
115
+ end
116
+
117
+ ##
118
+ # When the completion was created, as a Unix timestamp (seconds).
119
+ # @return [Integer, nil]
120
+ #
121
+ def created
122
+ self["created"]
123
+ end
124
+
125
+ ##
126
+ # The model producing the completion.
127
+ # @return [String, nil]
128
+ #
129
+ def model
130
+ self["model"]
131
+ end
132
+
133
+ ##
134
+ # The incremental text carried by this chunk — the first choice's
135
+ # `delta.content`. `nil` on chunks that carry no text (e.g. the opening
136
+ # role chunk and the final chunk).
137
+ # @return [String, nil]
138
+ #
139
+ def delta
140
+ first_delta["content"]
141
+ end
142
+
143
+ ##
144
+ # The author role, present on the opening chunk — the first choice's
145
+ # `delta.role`.
146
+ # @return [String, nil]
147
+ #
148
+ def role
149
+ first_delta["role"]
150
+ end
151
+
152
+ ##
153
+ # Why generation stopped, present on the final chunk — the first
154
+ # choice's `finish_reason`.
155
+ # @return [String, nil]
156
+ #
157
+ def finish_reason
158
+ first_choice["finish_reason"]
159
+ end
160
+
161
+ ##
162
+ # The token usage, present on the final chunk, wrapped in a
163
+ # {ChatUsage}. Returns `nil` when absent.
164
+ # @return [ChatUsage, nil]
165
+ #
166
+ def usage
167
+ raw = self["usage"]
168
+ raw.is_a?(::Hash) ? ChatUsage.new(raw) : nil
169
+ end
170
+
171
+ private
172
+
173
+ ##
174
+ # @return [Hash] The first choice, or an empty hash.
175
+ #
176
+ def first_choice
177
+ choices = self["choices"]
178
+ (choices.is_a?(::Array) ? choices.first : nil) || {}
179
+ end
180
+
181
+ ##
182
+ # @return [Hash] The first choice's delta, or an empty hash.
183
+ #
184
+ def first_delta
185
+ delta = first_choice["delta"]
186
+ delta.is_a?(::Hash) ? delta : {}
187
+ end
188
+ end
189
+
190
+ ##
191
+ # A custom `hermes.tool.progress` event emitted on the chat-completions
192
+ # stream while the server agent executes a tool. It is a distinct event
193
+ # type from {ChatCompletionChunk} — it carries no `choices`/`delta` and is
194
+ # never folded into the assembled {ChatCompletion} — so tool activity does
195
+ # not pollute the assistant text. The Responses API does not emit these
196
+ # (it represents tool activity as `function_call` output items instead).
197
+ #
198
+ # Each tool call produces two events keyed by {#tool_call_id}: a
199
+ # `"running"` event carrying {#emoji} and {#label}, then a `"completed"`
200
+ # event that omits them. `status` is a lifecycle marker only — a tool that
201
+ # fails (or times out) still reports `"completed"`; the failure surfaces in
202
+ # the tool's result, not here.
203
+ #
204
+ class ChatToolProgress < Entity
205
+ ##
206
+ # The tool name, e.g. `"search_files"` or `"terminal"`.
207
+ # @return [String, nil]
208
+ #
209
+ def tool
210
+ self["tool"]
211
+ end
212
+
213
+ ##
214
+ # A decorative emoji for the tool, present on the `"running"` event.
215
+ # @return [String, nil]
216
+ #
217
+ def emoji
218
+ self["emoji"]
219
+ end
220
+
221
+ ##
222
+ # A short human-facing descriptor of the invocation (e.g. the search
223
+ # glob `"*"`, or the command `"ls -F"`), present on the `"running"`
224
+ # event.
225
+ # @return [String, nil]
226
+ #
227
+ def label
228
+ self["label"]
229
+ end
230
+
231
+ ##
232
+ # The id correlating this event's `"running"` and `"completed"` frames
233
+ # (read from the camelCase `toolCallId` wire field), e.g. `"call_…"`.
234
+ # @return [String, nil]
235
+ #
236
+ def tool_call_id
237
+ self["toolCallId"]
238
+ end
239
+
240
+ ##
241
+ # The lifecycle status, `"running"` or `"completed"`.
242
+ # @return [String, nil]
243
+ #
244
+ def status
245
+ self["status"]
246
+ end
247
+
248
+ ##
249
+ # Whether this event marks the tool starting to run.
250
+ # @return [boolean]
251
+ #
252
+ def running?
253
+ status == "running"
254
+ end
255
+
256
+ ##
257
+ # Whether this event marks the tool finishing execution. Note this is a
258
+ # lifecycle marker, not a success signal — see the class docs.
259
+ # @return [boolean]
260
+ #
261
+ def completed?
262
+ status == "completed"
263
+ end
264
+ end
265
+
266
+ ##
267
+ # The result of a chat completion (`POST /v1/chat/completions`).
268
+ # Field readers are best-effort; {#to_h} remains the source of truth.
269
+ #
270
+ class ChatCompletion < Entity
271
+ include SessionHeaders
272
+
273
+ ##
274
+ # Reconstruct a completion from the events of a streamed turn. Chat
275
+ # streaming does not send a final aggregate object, so this assembles
276
+ # one: the message text is the concatenation of every chunk's
277
+ # `delta.content`, the role and finish_reason are taken from the chunks
278
+ # that carry them, and the usage from the final chunk. Single-choice
279
+ # (`choices[0]`) is assumed. Non-chunk events (e.g. {ChatToolProgress})
280
+ # are ignored, so the assembled text holds only the assistant's reply.
281
+ #
282
+ # @param events [Array<Entity>] The streamed events, in order; only
283
+ # {ChatCompletionChunk}s contribute to the result.
284
+ # @param session_id [String, nil] The session id from the response
285
+ # headers, carried onto the assembled completion.
286
+ # @param session_key [String, nil] The session key from the response
287
+ # headers, carried onto the assembled completion.
288
+ # @return [ChatCompletion]
289
+ #
290
+ def self.from_chunks(events, session_id: nil, session_key: nil)
291
+ chunks = events.select { |event| event.is_a?(ChatCompletionChunk) }
292
+ first = chunks.empty? ? {} : chunks.first.to_h
293
+ content = +""
294
+ role = nil
295
+ finish_reason = nil
296
+ usage = nil
297
+ chunks.each do |chunk|
298
+ role ||= chunk.role
299
+ content << chunk.delta if chunk.delta
300
+ finish_reason = chunk.finish_reason if chunk.finish_reason
301
+ usage = chunk["usage"] if chunk["usage"]
302
+ end
303
+ new(
304
+ {"id" => first["id"], "object" => "chat.completion",
305
+ "created" => first["created"], "model" => first["model"],
306
+ "choices" => [{"index" => 0,
307
+ "message" => {"role" => role, "content" => content},
308
+ "finish_reason" => finish_reason}],
309
+ "usage" => usage},
310
+ session_id: session_id, session_key: session_key
311
+ )
312
+ end
313
+
314
+ ##
315
+ # The completion id, e.g. `"chatcmpl-…"`.
316
+ # @return [String, nil]
317
+ #
318
+ def id
319
+ self["id"]
320
+ end
321
+
322
+ ##
323
+ # The object type, `"chat.completion"`.
324
+ # @return [String, nil]
325
+ #
326
+ def object
327
+ self["object"]
328
+ end
329
+
330
+ ##
331
+ # When the completion was created, as a Unix timestamp (seconds).
332
+ # @return [Integer, nil]
333
+ #
334
+ def created
335
+ self["created"]
336
+ end
337
+
338
+ ##
339
+ # The model that produced the completion, e.g. `"hermes-test"`.
340
+ # @return [String, nil]
341
+ #
342
+ def model
343
+ self["model"]
344
+ end
345
+
346
+ ##
347
+ # The generated choices, each wrapped in a {ChatChoice}. Returns `nil`
348
+ # when the field is absent.
349
+ # @return [Array<ChatChoice>, nil]
350
+ #
351
+ def choices
352
+ raw = self["choices"]
353
+ return nil unless raw.is_a?(::Array)
354
+
355
+ raw.map { |item| ChatChoice.new(item) }
356
+ end
357
+
358
+ ##
359
+ # The token usage, wrapped in a {ChatUsage}. Returns `nil` when the
360
+ # field is absent.
361
+ # @return [ChatUsage, nil]
362
+ #
363
+ def usage
364
+ raw = self["usage"]
365
+ raw.is_a?(::Hash) ? ChatUsage.new(raw) : nil
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hermes_agent/client/entity"
4
+
5
+ module HermesAgent
6
+ class Client
7
+ ##
8
+ # Wrapper objects for the payloads the server returns. Each is a subclass
9
+ # of {Entity}.
10
+ #
11
+ module Entities
12
+ ##
13
+ # The result of a server health check.
14
+ #
15
+ class Health < Entity
16
+ ##
17
+ # The reported health status, e.g. `"ok"`.
18
+ # @return [String, nil]
19
+ #
20
+ def status
21
+ self["status"]
22
+ end
23
+ end
24
+
25
+ ##
26
+ # The connection status of a single platform within a detailed health
27
+ # check (one entry of {HealthDetails#platforms}).
28
+ #
29
+ class PlatformStatus < Entity
30
+ ##
31
+ # The connection state, e.g. `"connected"`.
32
+ # @return [String, nil]
33
+ #
34
+ def state
35
+ self["state"]
36
+ end
37
+
38
+ ##
39
+ # A machine-readable error code, or `nil` when there is no error.
40
+ # @return [String, nil]
41
+ #
42
+ def error_code
43
+ self["error_code"]
44
+ end
45
+
46
+ ##
47
+ # A human-readable error message, or `nil` when there is no error.
48
+ # @return [String, nil]
49
+ #
50
+ def error_message
51
+ self["error_message"]
52
+ end
53
+
54
+ ##
55
+ # When this platform status was last updated (ISO-8601 timestamp
56
+ # string).
57
+ # @return [String, nil]
58
+ #
59
+ def updated_at
60
+ self["updated_at"]
61
+ end
62
+ end
63
+
64
+ ##
65
+ # The result of a detailed server health check (`/health/detailed`).
66
+ # Field readers are best-effort; {#to_h} remains the source of truth.
67
+ #
68
+ class HealthDetails < Entity
69
+ ##
70
+ # The reported health status, e.g. `"ok"`.
71
+ # @return [String, nil]
72
+ #
73
+ def status
74
+ self["status"]
75
+ end
76
+
77
+ ##
78
+ # The platform identifier, e.g. `"hermes-agent"`.
79
+ # @return [String, nil]
80
+ #
81
+ def platform
82
+ self["platform"]
83
+ end
84
+
85
+ ##
86
+ # The gateway's lifecycle state, e.g. `"running"`.
87
+ # @return [String, nil]
88
+ #
89
+ def gateway_state
90
+ self["gateway_state"]
91
+ end
92
+
93
+ ##
94
+ # The per-platform connection state, keyed by platform name (e.g.
95
+ # `"api_server"`), each value wrapped in a {PlatformStatus}. Returns
96
+ # `nil` when the field is absent.
97
+ # @return [Hash{String => PlatformStatus}, nil]
98
+ #
99
+ def platforms
100
+ raw = self["platforms"]
101
+ return nil unless raw.is_a?(::Hash)
102
+
103
+ raw.transform_values { |value| PlatformStatus.new(value) }
104
+ end
105
+
106
+ ##
107
+ # The number of agents currently running on the server.
108
+ # @return [Integer, nil]
109
+ #
110
+ def active_agents
111
+ self["active_agents"]
112
+ end
113
+
114
+ ##
115
+ # The reason the gateway exited, or `nil` while it is running.
116
+ # @return [String, nil]
117
+ #
118
+ def exit_reason
119
+ self["exit_reason"]
120
+ end
121
+
122
+ ##
123
+ # When this health snapshot was produced (ISO-8601 timestamp string).
124
+ # @return [String, nil]
125
+ #
126
+ def updated_at
127
+ self["updated_at"]
128
+ end
129
+
130
+ ##
131
+ # The process id of the running gateway.
132
+ # @return [Integer, nil]
133
+ #
134
+ def pid
135
+ self["pid"]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end