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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c8205d1a6aed4260e80ca3d351772791025195265b92a883b379a1f697c098a
|
|
4
|
+
data.tar.gz: 66bc29e492989c686d9866633e119b5cd2bdc2a4c8b1cc8fc34a0d9dc8b9d615
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 741e6c38b1937b260ddd206449ba5bf6095eb3f3ad037ba569a6eed6cc924c5b734805fc9ede3a043bc21359dc95a32f4abb1f2a55d89118ec1267d16c029da1
|
|
7
|
+
data.tar.gz: b006e53068f75e41434c913105956dac7335b1a49427daa82e7b29dd537152ec9decc50ef506d739378c3f5df20f9429cd9ce09b120f03c6f853c8a9fea3f829
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Daniel Azuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hermes-Client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
reserve the gem "hermes-client".
|
|
5
|
-
The actual gem is planned for release in the near future.
|
|
3
|
+
`hermes-client` is a Ruby client library for the Hermes Agent API Server.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
Install the gem using
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
gem install hermes-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your bundle:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# In your Gemfile
|
|
17
|
+
gem "hermes-client"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Create and use a client object:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "hermes-client"
|
|
24
|
+
|
|
25
|
+
# Create a new client and point it at a Hermes Gateway server
|
|
26
|
+
hermes_client = HermesAgent::Client.new(
|
|
27
|
+
base_url: "http://localhost:8642",
|
|
28
|
+
api_key: "my-key-12345678"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Send chat messages to the gateway
|
|
32
|
+
response = hermes_client.responses.create(input: "Tell me a joke.")
|
|
33
|
+
puts response.output_text
|
|
34
|
+
|
|
35
|
+
# Manage jobs
|
|
36
|
+
briefing_job = hermes_client.jobs.create(
|
|
37
|
+
name: "daily-briefing",
|
|
38
|
+
schedule: "every morning at 8am",
|
|
39
|
+
prompt: "Collect the day's news and email me a summary."
|
|
40
|
+
)
|
|
41
|
+
puts "Daily-briefing will next run at #{briefing_job.next_run_at}"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A client is not thread-safe (it holds a persistent connection); create one
|
|
45
|
+
client per thread.
|
|
46
|
+
|
|
47
|
+
For more information, see the
|
|
48
|
+
[Hermes Gateway API documentation](https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server).
|
|
49
|
+
|
|
50
|
+
Full API documentation is available at https://dazuma.github.io/hermes-client
|
|
51
|
+
for released gems.
|
|
52
|
+
|
|
53
|
+
## Requirements and status
|
|
54
|
+
|
|
55
|
+
`hermes-client` requires Ruby 3.4 or later.
|
|
56
|
+
|
|
57
|
+
The gem can be considered alpha quality. Initial development is complete, but
|
|
58
|
+
the library has seen minimal real-world testing, and it may experience
|
|
59
|
+
significant changes, including breaking interface changes. It is available now
|
|
60
|
+
on an experimental basis, but not currently recommended for production use.
|
|
61
|
+
|
|
62
|
+
## Contributing
|
|
63
|
+
|
|
64
|
+
Development is done in GitHub at https://github.com/dazuma/hermes-client.
|
|
65
|
+
|
|
66
|
+
* To file issues: https://github.com/dazuma/hermes-client/issues.
|
|
67
|
+
* For questions and discussion, please do not file an issue. Instead, use the
|
|
68
|
+
discussions feature: https://github.com/dazuma/hermes-client/discussions.
|
|
69
|
+
* Before opening any non-trivial pull request, please report a bug or feature
|
|
70
|
+
request using an issue.
|
|
71
|
+
|
|
72
|
+
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
|
73
|
+
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
|
74
|
+
unit tests, rubocop, and build tests independently.
|
|
75
|
+
|
|
76
|
+
As of late May, 2026, the documentation provided by Hermes is fairly thin, and
|
|
77
|
+
the developer had to cobble together an understanding of the API interfaces and
|
|
78
|
+
protocols from various sources, including the Hermes source and empirical
|
|
79
|
+
probing of a live gateway. Documents related to our findings are available in
|
|
80
|
+
the `devdocs` directory, and some Toys-based probing tools are also included in
|
|
81
|
+
this repository. (These are not included in the gem distribution.)
|
|
82
|
+
|
|
83
|
+
Much of the heavy lifting in the original implementation and documentation, as
|
|
84
|
+
well as the research behind it into the actual behavior of the gateway API, was
|
|
85
|
+
done in close collaboration with Claude Code (Opus 4.7).
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
Copyright 2026 Daniel Azuma
|
|
90
|
+
|
|
91
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
92
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
93
|
+
in the Software without restriction, including without limitation the rights
|
|
94
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
95
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
96
|
+
furnished to do so, subject to the following conditions:
|
|
97
|
+
|
|
98
|
+
The above copyright notice and this permission notice shall be included in
|
|
99
|
+
all copies or substantial portions of the Software.
|
|
100
|
+
|
|
101
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
102
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
103
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
104
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
105
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
106
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
107
|
+
IN THE SOFTWARE.
|
data/lib/hermes-client.rb
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# The actual gem is planned for release in the near future.
|
|
5
|
-
# If this is a problem, or if the actual gem has not been
|
|
6
|
-
# released in a timely manner, you can contact the owner at
|
|
7
|
-
# dazuma@gmail.com
|
|
8
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hermes_agent/client"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HermesAgent
|
|
4
|
+
class Client
|
|
5
|
+
##
|
|
6
|
+
# Connection settings for a {HermesAgent::Client}.
|
|
7
|
+
#
|
|
8
|
+
# Holds the server location and credentials shared by all requests. An
|
|
9
|
+
# instance is created when a client is constructed and may be customized
|
|
10
|
+
# either via keyword arguments or by yielding the configuration to a block.
|
|
11
|
+
#
|
|
12
|
+
class Configuration
|
|
13
|
+
##
|
|
14
|
+
# The default server root URL.
|
|
15
|
+
# @return [String]
|
|
16
|
+
#
|
|
17
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:8642"
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# The default keep-alive timeout, in seconds.
|
|
21
|
+
# @return [Numeric]
|
|
22
|
+
#
|
|
23
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 5
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Create a configuration.
|
|
27
|
+
#
|
|
28
|
+
# @param base_url [String] The server root URL, _without_ a path prefix
|
|
29
|
+
# such as `/v1`. Defaults to {DEFAULT_BASE_URL}.
|
|
30
|
+
# @param api_key [String, nil] The bearer token sent on every request, or
|
|
31
|
+
# `nil` to send no `Authorization` header. Defaults to the
|
|
32
|
+
# `HERMES_API_KEY` environment variable.
|
|
33
|
+
# @param read_timeout [Numeric, nil] The read timeout in seconds, or `nil`
|
|
34
|
+
# for no client-side limit.
|
|
35
|
+
# @param open_timeout [Numeric, nil] The connection-open timeout in
|
|
36
|
+
# seconds, or `nil` for no client-side limit.
|
|
37
|
+
# @param write_timeout [Numeric, nil] The timeout in seconds for writing
|
|
38
|
+
# a request, or `nil` for no client-side limit.
|
|
39
|
+
# @param keep_alive_timeout [Numeric] How long, in seconds, an idle
|
|
40
|
+
# persistent connection may be reused before it is considered stale
|
|
41
|
+
# and reopened on the next request. Defaults to
|
|
42
|
+
# {DEFAULT_KEEP_ALIVE_TIMEOUT}.
|
|
43
|
+
#
|
|
44
|
+
def initialize(base_url: DEFAULT_BASE_URL,
|
|
45
|
+
api_key: ::ENV.fetch("HERMES_API_KEY", nil),
|
|
46
|
+
read_timeout: nil,
|
|
47
|
+
open_timeout: nil,
|
|
48
|
+
write_timeout: nil,
|
|
49
|
+
keep_alive_timeout: DEFAULT_KEEP_ALIVE_TIMEOUT)
|
|
50
|
+
@base_url = base_url
|
|
51
|
+
@api_key = api_key
|
|
52
|
+
@read_timeout = read_timeout
|
|
53
|
+
@open_timeout = open_timeout
|
|
54
|
+
@write_timeout = write_timeout
|
|
55
|
+
@keep_alive_timeout = keep_alive_timeout
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# The server root URL, without a path prefix.
|
|
60
|
+
# @return [String]
|
|
61
|
+
#
|
|
62
|
+
attr_accessor :base_url
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# The bearer token sent on every request, or `nil` for none.
|
|
66
|
+
# @return [String, nil]
|
|
67
|
+
#
|
|
68
|
+
attr_accessor :api_key
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# The read timeout in seconds, or `nil` for no client-side limit.
|
|
72
|
+
# @return [Numeric, nil]
|
|
73
|
+
#
|
|
74
|
+
attr_accessor :read_timeout
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# The connection-open timeout in seconds, or `nil` for no client-side
|
|
78
|
+
# limit.
|
|
79
|
+
# @return [Numeric, nil]
|
|
80
|
+
#
|
|
81
|
+
attr_accessor :open_timeout
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# The request-write timeout in seconds, or `nil` for no client-side
|
|
85
|
+
# limit.
|
|
86
|
+
# @return [Numeric, nil]
|
|
87
|
+
#
|
|
88
|
+
attr_accessor :write_timeout
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# How long, in seconds, an idle persistent connection may be reused before
|
|
92
|
+
# it is considered stale and reopened on the next request.
|
|
93
|
+
# @return [Numeric]
|
|
94
|
+
#
|
|
95
|
+
attr_accessor :keep_alive_timeout
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HermesAgent
|
|
4
|
+
class Client
|
|
5
|
+
##
|
|
6
|
+
# A stateful, multi-turn conversation over the Responses API that chains its
|
|
7
|
+
# turns automatically, so each call takes only the turn's `input:`.
|
|
8
|
+
#
|
|
9
|
+
# Construct one via {Resources::Responses#conversation} rather than directly:
|
|
10
|
+
#
|
|
11
|
+
# convo = client.responses.conversation
|
|
12
|
+
# convo.create(input: "Hello").output_text
|
|
13
|
+
# convo.create(input: "And what about X?").output_text # auto-chains
|
|
14
|
+
#
|
|
15
|
+
# There are two chaining mechanisms, selected at construction:
|
|
16
|
+
#
|
|
17
|
+
# - **id-tracking mode** (the default): the conversation remembers each
|
|
18
|
+
# turn's response id client-side and threads it into the next turn as
|
|
19
|
+
# `previous_response_id`. Pass `previous_response_id:` to resume such a
|
|
20
|
+
# thread from a known id (e.g. across process restarts).
|
|
21
|
+
# - **named mode** (`name:`): every turn sends a stable `conversation` name
|
|
22
|
+
# and the server keeps the thread; no client-side id is threaded.
|
|
23
|
+
#
|
|
24
|
+
# The verb methods mirror {Resources::Responses} ({#create} /
|
|
25
|
+
# {#stream_create}) and return the same entities and stream, so the helper
|
|
26
|
+
# is a drop-in. {#last_response_id} is recorded in both modes for
|
|
27
|
+
# inspection or persistence.
|
|
28
|
+
#
|
|
29
|
+
# A conversation models a single sequential thread and is not thread-safe:
|
|
30
|
+
# issue and (for streaming) consume one turn before starting the next.
|
|
31
|
+
#
|
|
32
|
+
class Conversation
|
|
33
|
+
##
|
|
34
|
+
# Create a conversation. Prefer {Resources::Responses#conversation}.
|
|
35
|
+
#
|
|
36
|
+
# @param responses [Resources::Responses] The responses resource to issue
|
|
37
|
+
# turns through.
|
|
38
|
+
# @param name [String, nil] A conversation name for server-side chaining.
|
|
39
|
+
# Mutually exclusive with `previous_response_id`.
|
|
40
|
+
# @param previous_response_id [String, nil] A prior response id to seed
|
|
41
|
+
# client-side chaining from. Mutually exclusive with `name`.
|
|
42
|
+
# @raise [ArgumentError] If both `name` and `previous_response_id` are
|
|
43
|
+
# given (they select different chaining mechanisms).
|
|
44
|
+
#
|
|
45
|
+
# @private
|
|
46
|
+
def initialize(responses, name: nil, previous_response_id: nil)
|
|
47
|
+
raise ::ArgumentError, "name and previous_response_id are mutually exclusive" if name && previous_response_id
|
|
48
|
+
|
|
49
|
+
@responses = responses
|
|
50
|
+
@name = name
|
|
51
|
+
@last_response_id = previous_response_id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# The conversation name, in named mode; `nil` in id-tracking mode.
|
|
56
|
+
# @return [String, nil]
|
|
57
|
+
#
|
|
58
|
+
attr_reader :name
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# The id of the most recent turn's response (also the seed id before any
|
|
62
|
+
# turn, in id-tracking mode). In named mode it is recorded for inspection
|
|
63
|
+
# but not used for chaining.
|
|
64
|
+
# @return [String, nil]
|
|
65
|
+
#
|
|
66
|
+
attr_reader :last_response_id
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# Create the next turn in the conversation.
|
|
70
|
+
#
|
|
71
|
+
# @param input [String, Array<Hash>] The turn's input (see
|
|
72
|
+
# {Resources::Responses#create}).
|
|
73
|
+
# @param extra [Hash] Additional request-body fields merged into the body.
|
|
74
|
+
# @return [Entities::Response] The response. Its id becomes
|
|
75
|
+
# {#last_response_id}.
|
|
76
|
+
# @raise [APIError] If the server returns a non-2xx response.
|
|
77
|
+
#
|
|
78
|
+
def create(input:, **extra)
|
|
79
|
+
response = @responses.create(input: input, **chaining, **extra)
|
|
80
|
+
capture(response)
|
|
81
|
+
response
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Create the next turn, streaming its events. Follows the same
|
|
86
|
+
# block-or-enumerator contract as {Resources::Responses#stream_create}:
|
|
87
|
+
# with a block, each event is yielded and the assembled
|
|
88
|
+
# {Entities::Response} is returned; without one, a {Stream} is returned.
|
|
89
|
+
# In either case the turn's response id is captured into
|
|
90
|
+
# {#last_response_id} when the stream's result is built (during
|
|
91
|
+
# consumption), so a subsequent turn chains onto it.
|
|
92
|
+
#
|
|
93
|
+
# @param input [String, Array<Hash>] The turn's input.
|
|
94
|
+
# @param extra [Hash] Additional request-body fields merged into the body.
|
|
95
|
+
# @yieldparam event [Entities::ResponseStreamEvent] Each streamed event.
|
|
96
|
+
# @return [Entities::Response, Stream] The assembled response when a block
|
|
97
|
+
# is given, otherwise the {Stream}.
|
|
98
|
+
# @raise [APIError] If the server returns a non-2xx response.
|
|
99
|
+
#
|
|
100
|
+
def stream_create(input:, **extra, &)
|
|
101
|
+
@responses.stream_response(
|
|
102
|
+
on_result: method(:capture), input: input, **chaining, **extra, &
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# The chaining fields for the next turn: the conversation name in named
|
|
110
|
+
# mode, the tracked previous response id in id-tracking mode, or none.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash]
|
|
113
|
+
#
|
|
114
|
+
def chaining
|
|
115
|
+
return {conversation: @name} if @name
|
|
116
|
+
return {previous_response_id: @last_response_id} if @last_response_id
|
|
117
|
+
|
|
118
|
+
{}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Record a turn's response id as {#last_response_id}, ignoring a missing
|
|
123
|
+
# id (which would otherwise drop a previously tracked one).
|
|
124
|
+
#
|
|
125
|
+
# @param response [Entities::Response, nil] The turn's response.
|
|
126
|
+
# @return [void]
|
|
127
|
+
#
|
|
128
|
+
def capture(response)
|
|
129
|
+
id = response&.id
|
|
130
|
+
@last_response_id = id if id
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
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 authentication scheme advertised by the server
|
|
10
|
+
# ({Capabilities#auth}).
|
|
11
|
+
#
|
|
12
|
+
class Auth < Entity
|
|
13
|
+
##
|
|
14
|
+
# The authentication type, e.g. `"bearer"`.
|
|
15
|
+
# @return [String, nil]
|
|
16
|
+
#
|
|
17
|
+
def type
|
|
18
|
+
self["type"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Whether authentication is required.
|
|
23
|
+
# @return [boolean, nil]
|
|
24
|
+
#
|
|
25
|
+
def required?
|
|
26
|
+
self["required"]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# The server's execution model ({Capabilities#runtime}).
|
|
32
|
+
#
|
|
33
|
+
class Runtime < Entity
|
|
34
|
+
##
|
|
35
|
+
# The runtime mode, e.g. `"server_agent"`.
|
|
36
|
+
# @return [String, nil]
|
|
37
|
+
#
|
|
38
|
+
def mode
|
|
39
|
+
self["mode"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Where tools execute, e.g. `"server"`.
|
|
44
|
+
# @return [String, nil]
|
|
45
|
+
#
|
|
46
|
+
def tool_execution
|
|
47
|
+
self["tool_execution"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Whether the runtime is split between client and server. A runtime
|
|
52
|
+
# that does not advertise this is treated as not split (`false`).
|
|
53
|
+
# @return [boolean]
|
|
54
|
+
#
|
|
55
|
+
def split_runtime?
|
|
56
|
+
!!self["split_runtime"]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# A human-readable description of the runtime.
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
#
|
|
63
|
+
def description
|
|
64
|
+
self["description"]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# The server's feature matrix ({Capabilities#features}).
|
|
70
|
+
#
|
|
71
|
+
# Each reader returns whether the server advertises that feature: `true`
|
|
72
|
+
# when the flag is set, `false` when it is unset or absent (an
|
|
73
|
+
# unadvertised feature is treated as unsupported). Readers are
|
|
74
|
+
# best-effort; use {#[]} / {#to_h} for any feature not yet modeled here.
|
|
75
|
+
#
|
|
76
|
+
class Features < Entity
|
|
77
|
+
##
|
|
78
|
+
# Whether the chat-completions endpoint is supported.
|
|
79
|
+
# @return [boolean]
|
|
80
|
+
#
|
|
81
|
+
def chat_completions?
|
|
82
|
+
!!self["chat_completions"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Whether chat-completions streaming is supported.
|
|
87
|
+
# @return [boolean]
|
|
88
|
+
#
|
|
89
|
+
def chat_completions_streaming?
|
|
90
|
+
!!self["chat_completions_streaming"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Whether the Responses API is supported.
|
|
95
|
+
# @return [boolean]
|
|
96
|
+
#
|
|
97
|
+
def responses_api?
|
|
98
|
+
!!self["responses_api"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Whether Responses API streaming is supported.
|
|
103
|
+
# @return [boolean]
|
|
104
|
+
#
|
|
105
|
+
def responses_streaming?
|
|
106
|
+
!!self["responses_streaming"]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Whether run submission is supported.
|
|
111
|
+
# @return [boolean]
|
|
112
|
+
#
|
|
113
|
+
def run_submission?
|
|
114
|
+
!!self["run_submission"]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Whether run status polling is supported.
|
|
119
|
+
# @return [boolean]
|
|
120
|
+
#
|
|
121
|
+
def run_status?
|
|
122
|
+
!!self["run_status"]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# Whether the run events SSE stream is supported.
|
|
127
|
+
# @return [boolean]
|
|
128
|
+
#
|
|
129
|
+
def run_events_sse?
|
|
130
|
+
!!self["run_events_sse"]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
# Whether stopping a run is supported.
|
|
135
|
+
# @return [boolean]
|
|
136
|
+
#
|
|
137
|
+
def run_stop?
|
|
138
|
+
!!self["run_stop"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
# Whether responding to a run approval request is supported.
|
|
143
|
+
# @return [boolean]
|
|
144
|
+
#
|
|
145
|
+
def run_approval_response?
|
|
146
|
+
!!self["run_approval_response"]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
# Whether the server emits custom tool-progress events.
|
|
151
|
+
# @return [boolean]
|
|
152
|
+
#
|
|
153
|
+
def tool_progress_events?
|
|
154
|
+
!!self["tool_progress_events"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
##
|
|
158
|
+
# Whether the server emits approval events.
|
|
159
|
+
# @return [boolean]
|
|
160
|
+
#
|
|
161
|
+
def approval_events?
|
|
162
|
+
!!self["approval_events"]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
##
|
|
166
|
+
# Whether CORS is enabled.
|
|
167
|
+
# @return [boolean]
|
|
168
|
+
#
|
|
169
|
+
def cors?
|
|
170
|
+
!!self["cors"]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
##
|
|
174
|
+
# The request header carrying the session-continuity id, e.g.
|
|
175
|
+
# `"X-Hermes-Session-Id"`.
|
|
176
|
+
# @return [String, nil]
|
|
177
|
+
#
|
|
178
|
+
def session_continuity_header
|
|
179
|
+
self["session_continuity_header"]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# The request header carrying the session key, e.g.
|
|
184
|
+
# `"X-Hermes-Session-Key"`.
|
|
185
|
+
# @return [String, nil]
|
|
186
|
+
#
|
|
187
|
+
def session_key_header
|
|
188
|
+
self["session_key_header"]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
##
|
|
193
|
+
# A single advertised route (one entry of {Capabilities#endpoints}).
|
|
194
|
+
#
|
|
195
|
+
class Endpoint < Entity
|
|
196
|
+
##
|
|
197
|
+
# The HTTP method, e.g. `"GET"`. (Named `http_method` rather than
|
|
198
|
+
# `method` to avoid shadowing `Object#method`.)
|
|
199
|
+
# @return [String, nil]
|
|
200
|
+
#
|
|
201
|
+
def http_method
|
|
202
|
+
self["method"]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
# The request path, e.g. `"/v1/models"`. May contain `{...}`
|
|
207
|
+
# placeholders such as `/v1/runs/{run_id}`.
|
|
208
|
+
# @return [String, nil]
|
|
209
|
+
#
|
|
210
|
+
def path
|
|
211
|
+
self["path"]
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# The server's advertised capabilities (`GET /v1/capabilities`).
|
|
217
|
+
# Field readers are best-effort; {#to_h} remains the source of truth.
|
|
218
|
+
#
|
|
219
|
+
class Capabilities < Entity
|
|
220
|
+
##
|
|
221
|
+
# The object type, `"hermes.api_server.capabilities"`.
|
|
222
|
+
# @return [String, nil]
|
|
223
|
+
#
|
|
224
|
+
def object
|
|
225
|
+
self["object"]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
# The platform identifier, e.g. `"hermes-agent"`.
|
|
230
|
+
# @return [String, nil]
|
|
231
|
+
#
|
|
232
|
+
def platform
|
|
233
|
+
self["platform"]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
##
|
|
237
|
+
# The configured server-side model id, e.g. `"hermes-test"`.
|
|
238
|
+
# @return [String, nil]
|
|
239
|
+
#
|
|
240
|
+
def model
|
|
241
|
+
self["model"]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# The authentication scheme, wrapped in an {Auth} entity. Returns
|
|
246
|
+
# `nil` when the field is absent.
|
|
247
|
+
# @return [Auth, nil]
|
|
248
|
+
#
|
|
249
|
+
def auth
|
|
250
|
+
raw = self["auth"]
|
|
251
|
+
raw.is_a?(::Hash) ? Auth.new(raw) : nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
##
|
|
255
|
+
# The execution model, wrapped in a {Runtime} entity. Returns `nil`
|
|
256
|
+
# when the field is absent.
|
|
257
|
+
# @return [Runtime, nil]
|
|
258
|
+
#
|
|
259
|
+
def runtime
|
|
260
|
+
raw = self["runtime"]
|
|
261
|
+
raw.is_a?(::Hash) ? Runtime.new(raw) : nil
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
##
|
|
265
|
+
# The feature matrix, wrapped in a {Features} entity. Returns `nil`
|
|
266
|
+
# when the field is absent.
|
|
267
|
+
# @return [Features, nil]
|
|
268
|
+
#
|
|
269
|
+
def features
|
|
270
|
+
raw = self["features"]
|
|
271
|
+
raw.is_a?(::Hash) ? Features.new(raw) : nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
##
|
|
275
|
+
# The advertised routes, keyed by logical name (e.g. `"models"`), each
|
|
276
|
+
# value wrapped in an {Endpoint}. Returns `nil` when the field is
|
|
277
|
+
# absent.
|
|
278
|
+
# @return [Hash{String => Endpoint}, nil]
|
|
279
|
+
#
|
|
280
|
+
def endpoints
|
|
281
|
+
raw = self["endpoints"]
|
|
282
|
+
return nil unless raw.is_a?(::Hash)
|
|
283
|
+
|
|
284
|
+
raw.transform_values { |value| Endpoint.new(value) }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|