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,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
|