@harapter/adapter-hermes 0.1.1
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.
- package/LICENSE +201 -0
- package/README.md +225 -0
- package/dist/adapter.d.ts +39 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +1368 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +120 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +736 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +53 -0
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
import { assertSessionCompatibility, assertSessionOwnership, ExtensionRegistry, HarnessError, providerSessionId, runId, } from '@harapter/core';
|
|
2
|
+
import { HttpSseTransport, HttpTransportError, } from '@harapter/transport-http-sse';
|
|
3
|
+
import { HERMES_PROVIDER_ID, HERMES_SESSION_COMPATIBILITY_REF, HERMES_SUBAGENT_EXTENSION, compatibilityFingerprint, mapHermesEvent, parseHermesApprovalResponse, parseHermesCapabilities, parseHermesRunReceipt, parseHermesRunStatus, parseHermesSession, parseHermesStopResponse, prepareHermesRun, prepareHermesSession, sessionStateFromRef, snapshotHermesSessionState, } from './protocol.js';
|
|
4
|
+
const descriptor = {
|
|
5
|
+
providerId: HERMES_PROVIDER_ID,
|
|
6
|
+
displayName: 'Hermes Agent',
|
|
7
|
+
connectionKinds: ['endpoint'],
|
|
8
|
+
documentationUrl: 'https://hermes-agent.nousresearch.com/docs/user-guide/features/api-server',
|
|
9
|
+
};
|
|
10
|
+
const defaultMaxRunEvents = 128;
|
|
11
|
+
const defaultCancelSettlementTimeoutMs = 10_000;
|
|
12
|
+
const defaultLateEventDrainTimeoutMs = 500;
|
|
13
|
+
const defaultReconcilePollIntervalMs = 100;
|
|
14
|
+
const defaultReconcileTimeoutMs = 30_000;
|
|
15
|
+
const maximumRunEventCapacity = 4096;
|
|
16
|
+
const maximumTimerMilliseconds = 2_147_483_647;
|
|
17
|
+
const uncertainTransportCodes = new Set([
|
|
18
|
+
'capacity_exceeded',
|
|
19
|
+
'network_failure',
|
|
20
|
+
'request_aborted',
|
|
21
|
+
'request_timeout',
|
|
22
|
+
'response_stream_failed',
|
|
23
|
+
'stream_ended',
|
|
24
|
+
'transport_closed',
|
|
25
|
+
]);
|
|
26
|
+
/** Create a fresh Hermes Agent API Server Adapter factory. */
|
|
27
|
+
export function createHermesProviderFactory(options = {}) {
|
|
28
|
+
return {
|
|
29
|
+
descriptor: () => ({
|
|
30
|
+
...descriptor,
|
|
31
|
+
connectionKinds: [...descriptor.connectionKinds],
|
|
32
|
+
}),
|
|
33
|
+
connect: async (profile) => connectHermes(profile, options),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function connectHermes(profile, factoryOptions) {
|
|
37
|
+
validateProfile(profile, factoryOptions);
|
|
38
|
+
const options = connectionOptions(profile.providerOptions);
|
|
39
|
+
const headers = await resolveHeaders(profile, factoryOptions);
|
|
40
|
+
let transport;
|
|
41
|
+
try {
|
|
42
|
+
transport = new HttpSseTransport({
|
|
43
|
+
baseUrl: endpointUrl(profile),
|
|
44
|
+
...(factoryOptions.fetch === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { fetch: factoryOptions.fetch }),
|
|
47
|
+
...(headers === undefined ? {} : { defaultHeaders: headers }),
|
|
48
|
+
...(options.requestTimeoutMs === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { requestTimeoutMs: options.requestTimeoutMs }),
|
|
51
|
+
...(options.sseConnectTimeoutMs === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { sseConnectTimeoutMs: options.sseConnectTimeoutMs }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw mapError(error, profile, 'configure transport', true);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const capabilities = parseHermesCapabilities(await requestProviderJson(transport, 'v1/capabilities', {}, profile, 'capability probe', 'compatibility', true));
|
|
61
|
+
return new HermesClient(snapshotProfile(profile), transport, capabilities, options);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
await transport.close().catch(() => undefined);
|
|
65
|
+
throw mapError(error, profile, 'connect', true);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
class HermesClient {
|
|
69
|
+
profile;
|
|
70
|
+
transport;
|
|
71
|
+
observedCapabilities;
|
|
72
|
+
options;
|
|
73
|
+
activeBySession = new Map();
|
|
74
|
+
startingBySession = new Map();
|
|
75
|
+
extensionRegistry = new ExtensionRegistry(HERMES_PROVIDER_ID);
|
|
76
|
+
fingerprint;
|
|
77
|
+
nativeClient;
|
|
78
|
+
pendingApprovals = new Map();
|
|
79
|
+
quarantinedSessions = new Set();
|
|
80
|
+
subagentListeners = new Set();
|
|
81
|
+
unknownListeners = new Set();
|
|
82
|
+
closePromise;
|
|
83
|
+
closed = false;
|
|
84
|
+
interactionSerial = 0;
|
|
85
|
+
runSerial = 0;
|
|
86
|
+
constructor(profile, transport, observedCapabilities, options) {
|
|
87
|
+
this.profile = profile;
|
|
88
|
+
this.transport = transport;
|
|
89
|
+
this.observedCapabilities = observedCapabilities;
|
|
90
|
+
this.options = options;
|
|
91
|
+
this.fingerprint = compatibilityFingerprint(observedCapabilities);
|
|
92
|
+
this.nativeClient = Object.freeze({
|
|
93
|
+
runtimeIdentity: this.runtimeIdentity(),
|
|
94
|
+
request: (path, requestOptions) => this.nativeRequest(path, requestOptions),
|
|
95
|
+
onUnknownEvent: (listener) => {
|
|
96
|
+
this.unknownListeners.add(listener);
|
|
97
|
+
return () => this.unknownListeners.delete(listener);
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const subagents = Object.freeze({
|
|
101
|
+
onEvent: (listener) => {
|
|
102
|
+
this.subagentListeners.add(listener);
|
|
103
|
+
return () => this.subagentListeners.delete(listener);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
this.extensionRegistry.register({
|
|
107
|
+
name: HERMES_SUBAGENT_EXTENSION,
|
|
108
|
+
providerId: HERMES_PROVIDER_ID,
|
|
109
|
+
displayName: 'Hermes child-session observer',
|
|
110
|
+
description: 'Observes bounded child-session lifecycle events without extending a parent Run trace.',
|
|
111
|
+
stability: 'experimental',
|
|
112
|
+
}, subagents);
|
|
113
|
+
}
|
|
114
|
+
descriptor() {
|
|
115
|
+
return Promise.resolve({
|
|
116
|
+
providerId: HERMES_PROVIDER_ID,
|
|
117
|
+
profileId: this.profile.profileId,
|
|
118
|
+
displayName: this.profile.displayName,
|
|
119
|
+
connectionKind: 'endpoint',
|
|
120
|
+
runtime: {
|
|
121
|
+
name: 'Hermes Agent API Server',
|
|
122
|
+
protocol: 'HTTP + SSE',
|
|
123
|
+
protocolVersion: this.fingerprint,
|
|
124
|
+
},
|
|
125
|
+
compatibility: 'experimental',
|
|
126
|
+
warnings: [
|
|
127
|
+
{
|
|
128
|
+
code: 'runtime_compatibility_unnegotiated',
|
|
129
|
+
message: 'Hermes Agent 0.21.0 was live-verified on 2026-09-03, but the API Server does not negotiate a Runtime compatibility version.',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
capabilities() {
|
|
135
|
+
return Promise.resolve(this.capabilityManifest());
|
|
136
|
+
}
|
|
137
|
+
async createSession(input = {}) {
|
|
138
|
+
this.assertOpen();
|
|
139
|
+
const prepared = prepareHermesSession(input);
|
|
140
|
+
try {
|
|
141
|
+
const response = await requestProviderJson(this.transport, 'api/sessions', {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: jsonHeaders,
|
|
144
|
+
body: jsonBody(prepared.body),
|
|
145
|
+
}, this.profile, 'create Session', 'session');
|
|
146
|
+
const session = parseHermesSession(response);
|
|
147
|
+
const sessionId = providerSessionId(session.id);
|
|
148
|
+
this.assertSessionReusable(sessionId);
|
|
149
|
+
if (prepared.state.model !== undefined &&
|
|
150
|
+
session.model !== undefined &&
|
|
151
|
+
prepared.state.model !== session.model) {
|
|
152
|
+
throw sessionMismatch(this.profile);
|
|
153
|
+
}
|
|
154
|
+
return new HermesSession(this, sessionId, prepared.state);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
throw mapError(error, this.profile, 'create Session');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async resumeSession(ref) {
|
|
161
|
+
this.assertOpen();
|
|
162
|
+
assertSessionOwnership(ref, HERMES_PROVIDER_ID, this.profile.profileId);
|
|
163
|
+
assertSessionCompatibility(ref, HERMES_SESSION_COMPATIBILITY_REF);
|
|
164
|
+
this.assertSessionReusable(ref.providerSessionId);
|
|
165
|
+
const state = sessionStateFromRef(ref);
|
|
166
|
+
try {
|
|
167
|
+
const session = parseHermesSession(await requestProviderJson(this.transport, sessionPath(ref.providerSessionId), {}, this.profile, 'resume Session', 'session'));
|
|
168
|
+
if (session.id !== ref.providerSessionId ||
|
|
169
|
+
(state.model !== undefined &&
|
|
170
|
+
session.model !== undefined &&
|
|
171
|
+
state.model !== session.model)) {
|
|
172
|
+
throw sessionMismatch(this.profile);
|
|
173
|
+
}
|
|
174
|
+
return new HermesSession(this, providerSessionId(session.id), state);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw mapError(error, this.profile, 'resume Session');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
extensions() {
|
|
181
|
+
return this.extensionRegistry;
|
|
182
|
+
}
|
|
183
|
+
native(guard) {
|
|
184
|
+
const value = this.nativeClient;
|
|
185
|
+
return guard !== undefined && !guard(value) ? undefined : value;
|
|
186
|
+
}
|
|
187
|
+
close() {
|
|
188
|
+
this.closePromise ??= this.closeOnce();
|
|
189
|
+
return this.closePromise;
|
|
190
|
+
}
|
|
191
|
+
sessionRef(sessionId, state) {
|
|
192
|
+
return {
|
|
193
|
+
providerId: HERMES_PROVIDER_ID,
|
|
194
|
+
profileId: this.profile.profileId,
|
|
195
|
+
providerSessionId: sessionId,
|
|
196
|
+
compatibilityRef: HERMES_SESSION_COMPATIBILITY_REF,
|
|
197
|
+
providerState: snapshotHermesSessionState(state),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
capabilityManifest() {
|
|
201
|
+
return hermesCapabilityManifest(this.profile, this.runtimeIdentity(), this.observedCapabilities);
|
|
202
|
+
}
|
|
203
|
+
async startRun(owner, sessionId, state, input, runOptions = {}) {
|
|
204
|
+
this.assertOpen();
|
|
205
|
+
this.assertSessionReusable(sessionId);
|
|
206
|
+
if (runOptions.timeoutMs !== undefined) {
|
|
207
|
+
validateRunTimeout(runOptions.timeoutMs);
|
|
208
|
+
}
|
|
209
|
+
if (this.activeBySession.has(sessionId) ||
|
|
210
|
+
this.startingBySession.has(sessionId)) {
|
|
211
|
+
throw new HarnessError('run_conflict', 'Hermes Agent Session already has an active Run.', {
|
|
212
|
+
retryable: false,
|
|
213
|
+
providerId: HERMES_PROVIDER_ID,
|
|
214
|
+
profileId: this.profile.profileId,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const request = prepareHermesRun(input, sessionId, state, runOptions);
|
|
218
|
+
const starting = {
|
|
219
|
+
closed: false,
|
|
220
|
+
controller: new AbortController(),
|
|
221
|
+
owner,
|
|
222
|
+
};
|
|
223
|
+
this.startingBySession.set(sessionId, starting);
|
|
224
|
+
let receipt;
|
|
225
|
+
let submissionAccepted = false;
|
|
226
|
+
try {
|
|
227
|
+
const response = await requestProviderJson(this.transport, 'v1/runs', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: jsonHeaders,
|
|
230
|
+
body: jsonBody(request),
|
|
231
|
+
signal: starting.controller.signal,
|
|
232
|
+
}, this.profile, 'start Run', 'run');
|
|
233
|
+
submissionAccepted = true;
|
|
234
|
+
receipt = parseHermesRunReceipt(response);
|
|
235
|
+
parseHermesRunStatus(await requestProviderJson(this.transport, runPath(receipt.runId), {}, this.profile, 'validate Run ownership', 'run'), receipt.runId, sessionId);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (this.startingBySession.get(sessionId) === starting) {
|
|
239
|
+
this.startingBySession.delete(sessionId);
|
|
240
|
+
}
|
|
241
|
+
const mapped = mapError(error, this.profile, 'start Run');
|
|
242
|
+
if (starting.closed ||
|
|
243
|
+
submissionAccepted ||
|
|
244
|
+
uncertainMutationFailure(mapped)) {
|
|
245
|
+
this.quarantinedSessions.add(sessionId);
|
|
246
|
+
owner.markUnsafe();
|
|
247
|
+
}
|
|
248
|
+
if (starting.closed)
|
|
249
|
+
throw sessionUnsafe(this.profile);
|
|
250
|
+
throw mapped;
|
|
251
|
+
}
|
|
252
|
+
if (starting.closed || this.startingBySession.get(sessionId) !== starting) {
|
|
253
|
+
if (this.startingBySession.get(sessionId) === starting) {
|
|
254
|
+
this.startingBySession.delete(sessionId);
|
|
255
|
+
}
|
|
256
|
+
this.quarantinedSessions.add(sessionId);
|
|
257
|
+
owner.markUnsafe();
|
|
258
|
+
throw sessionUnsafe(this.profile);
|
|
259
|
+
}
|
|
260
|
+
const run = new HermesRun({
|
|
261
|
+
providerId: HERMES_PROVIDER_ID,
|
|
262
|
+
profileId: this.profile.profileId,
|
|
263
|
+
sessionId,
|
|
264
|
+
runId: runId(`hermes-run-${String(++this.runSerial)}`),
|
|
265
|
+
providerRunId: receipt.runId,
|
|
266
|
+
}, owner, receipt.runId, runOptions.timeoutMs, this.transport, this.profile, this.options, this.observedCapabilities.features.cancel, (activeRun, mapping) => {
|
|
267
|
+
this.onRunMapping(activeRun, mapping);
|
|
268
|
+
}, (activeRun) => {
|
|
269
|
+
this.onRunSettling(activeRun);
|
|
270
|
+
}, (activeRun) => {
|
|
271
|
+
this.markSessionUnsafe(activeRun);
|
|
272
|
+
});
|
|
273
|
+
this.startingBySession.delete(sessionId);
|
|
274
|
+
this.activeBySession.set(sessionId, run);
|
|
275
|
+
run.open();
|
|
276
|
+
return run;
|
|
277
|
+
}
|
|
278
|
+
async respond(owner, sessionId, requestId, response) {
|
|
279
|
+
this.assertOpen();
|
|
280
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
281
|
+
if (pending?.run.owner !== owner ||
|
|
282
|
+
pending.run.ref().sessionId !== sessionId ||
|
|
283
|
+
pending.run.isTerminal() ||
|
|
284
|
+
pending.claimed) {
|
|
285
|
+
throw invalidInteraction(this.profile);
|
|
286
|
+
}
|
|
287
|
+
const choice = approvalChoice(response);
|
|
288
|
+
if (!pending.choices.has(choice)) {
|
|
289
|
+
throw invalidInteraction(this.profile);
|
|
290
|
+
}
|
|
291
|
+
pending.claimed = true;
|
|
292
|
+
const settleApprovalResponse = pending.run.beginApprovalResponse();
|
|
293
|
+
try {
|
|
294
|
+
parseHermesApprovalResponse(await requestProviderJson(this.transport, `${runPath(pending.run.providerRunId)}/approval`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: jsonHeaders,
|
|
297
|
+
body: jsonBody({ choice }),
|
|
298
|
+
}, this.profile, 'respond to approval', 'interaction'), pending.run.providerRunId, choice);
|
|
299
|
+
if (pending.providerResolvedChoice !== undefined) {
|
|
300
|
+
if (pending.providerResolvedChoice !== choice) {
|
|
301
|
+
pending.run.failIncompatible('contradictory approval resolution');
|
|
302
|
+
throw providerIncompatible(this.profile, 'contradictory approval resolution');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
if (!pending.run.confirmApproval(choice)) {
|
|
307
|
+
pending.run.failIncompatible('overlapping approval acknowledgement');
|
|
308
|
+
throw providerIncompatible(this.profile, 'overlapping approval acknowledgement');
|
|
309
|
+
}
|
|
310
|
+
this.resolveApproval(pending.run, 'host');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
const mapped = mapError(error, this.profile, 'respond to approval');
|
|
315
|
+
if (uncertainMutationFailure(mapped)) {
|
|
316
|
+
this.markSessionUnsafe(pending.run);
|
|
317
|
+
pending.run.abortConnection();
|
|
318
|
+
throw mapped;
|
|
319
|
+
}
|
|
320
|
+
if (this.pendingApprovals.get(requestId) === pending &&
|
|
321
|
+
!pending.run.isTerminal()) {
|
|
322
|
+
pending.claimed = false;
|
|
323
|
+
}
|
|
324
|
+
throw mapped;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
settleApprovalResponse();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
closeSession(owner, sessionId) {
|
|
331
|
+
const starting = this.startingBySession.get(sessionId);
|
|
332
|
+
if (starting?.owner === owner) {
|
|
333
|
+
starting.closed = true;
|
|
334
|
+
starting.controller.abort();
|
|
335
|
+
}
|
|
336
|
+
const active = this.activeBySession.get(sessionId);
|
|
337
|
+
if (active?.owner === owner)
|
|
338
|
+
active.abortConnection();
|
|
339
|
+
}
|
|
340
|
+
onRunMapping(run, mapping) {
|
|
341
|
+
if (mapping.kind === 'interaction') {
|
|
342
|
+
if (!this.observedCapabilities.features.approval) {
|
|
343
|
+
run.failIncompatible('approval event without advertised support');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if ([...this.pendingApprovals.values()].some((pending) => pending.run === run)) {
|
|
347
|
+
run.failIncompatible('overlapping approval requests');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const localId = `hermes-interaction-${String(++this.interactionSerial)}`;
|
|
351
|
+
this.pendingApprovals.set(localId, {
|
|
352
|
+
claimed: false,
|
|
353
|
+
choices: new Set(mapping.choices),
|
|
354
|
+
localId,
|
|
355
|
+
run,
|
|
356
|
+
});
|
|
357
|
+
run.emit({
|
|
358
|
+
type: 'interaction.requested',
|
|
359
|
+
data: { ...mapping.request, requestId: localId },
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (mapping.kind === 'interaction.resolved') {
|
|
364
|
+
const pending = this.pendingApproval(run);
|
|
365
|
+
if (pending !== undefined) {
|
|
366
|
+
if (!pending.choices.has(mapping.choice)) {
|
|
367
|
+
run.failIncompatible('approval resolution outside requested choices');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (pending.claimed) {
|
|
371
|
+
pending.providerResolvedChoice = mapping.choice;
|
|
372
|
+
}
|
|
373
|
+
this.resolveApproval(run, 'provider');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!run.acceptConfirmedApproval(mapping.choice)) {
|
|
377
|
+
run.failIncompatible('approval resolution without matching request');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (mapping.kind === 'subagent') {
|
|
383
|
+
this.emitSubagent(mapping.event);
|
|
384
|
+
if (!run.hasTerminalBoundary()) {
|
|
385
|
+
run.emit({
|
|
386
|
+
type: 'provider',
|
|
387
|
+
data: { childSessionId: mapping.event.childSessionId },
|
|
388
|
+
providerEventType: mapping.event.eventType,
|
|
389
|
+
raw: mapping.event.raw,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (mapping.kind === 'provider') {
|
|
395
|
+
this.emitUnknown(mapping.event);
|
|
396
|
+
if (!run.hasTerminalBoundary()) {
|
|
397
|
+
run.emit({
|
|
398
|
+
type: 'provider',
|
|
399
|
+
data: {},
|
|
400
|
+
providerEventType: mapping.event.providerEventType,
|
|
401
|
+
raw: mapping.event.raw,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
resolveApproval(run, resolution) {
|
|
407
|
+
const pending = this.pendingApproval(run);
|
|
408
|
+
if (pending === undefined)
|
|
409
|
+
return;
|
|
410
|
+
this.pendingApprovals.delete(pending.localId);
|
|
411
|
+
run.emitSettlement({
|
|
412
|
+
type: 'interaction.resolved',
|
|
413
|
+
data: { requestId: pending.localId, resolution },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
pendingApproval(run) {
|
|
417
|
+
return [...this.pendingApprovals.values()].find((candidate) => candidate.run === run);
|
|
418
|
+
}
|
|
419
|
+
onRunSettling(run) {
|
|
420
|
+
const sessionId = run.ref().sessionId;
|
|
421
|
+
if (this.activeBySession.get(sessionId) === run) {
|
|
422
|
+
this.activeBySession.delete(sessionId);
|
|
423
|
+
}
|
|
424
|
+
this.resolveApproval(run, 'terminal');
|
|
425
|
+
}
|
|
426
|
+
emitSubagent(event) {
|
|
427
|
+
for (const listener of this.subagentListeners) {
|
|
428
|
+
try {
|
|
429
|
+
listener(structuredClone(event));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Host observers cannot alter parent Run settlement.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
emitUnknown(event) {
|
|
437
|
+
for (const listener of this.unknownListeners) {
|
|
438
|
+
try {
|
|
439
|
+
listener(structuredClone(event));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Host observers cannot alter Provider lifecycle settlement.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
markSessionUnsafe(run) {
|
|
447
|
+
const sessionId = run.ref().sessionId;
|
|
448
|
+
this.quarantinedSessions.add(sessionId);
|
|
449
|
+
run.owner.markUnsafe();
|
|
450
|
+
}
|
|
451
|
+
assertSessionReusable(sessionId) {
|
|
452
|
+
if (!this.quarantinedSessions.has(sessionId))
|
|
453
|
+
return;
|
|
454
|
+
throw sessionUnsafe(this.profile);
|
|
455
|
+
}
|
|
456
|
+
async nativeRequest(path, options = {}) {
|
|
457
|
+
this.assertOpen();
|
|
458
|
+
const requestOptions = {
|
|
459
|
+
...(options.method === undefined ? {} : { method: options.method }),
|
|
460
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
461
|
+
...(options.timeoutMs === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { timeoutMs: options.timeoutMs }),
|
|
464
|
+
...(options.body === undefined
|
|
465
|
+
? {}
|
|
466
|
+
: { body: jsonBody(options.body), headers: jsonHeaders }),
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
const response = await this.transport.request(path, requestOptions);
|
|
470
|
+
return {
|
|
471
|
+
status: response.status,
|
|
472
|
+
body: parseJsonResponse(response, 'native response'),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
throw mapError(error, this.profile, 'native request');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
runtimeIdentity() {
|
|
480
|
+
return `${HERMES_SESSION_COMPATIBILITY_REF};${this.fingerprint}`;
|
|
481
|
+
}
|
|
482
|
+
assertOpen() {
|
|
483
|
+
if (!this.closed && this.transport.isOpen())
|
|
484
|
+
return;
|
|
485
|
+
throw new HarnessError('connection_aborted', 'The Hermes Agent Client connection is closed.', {
|
|
486
|
+
retryable: false,
|
|
487
|
+
providerId: HERMES_PROVIDER_ID,
|
|
488
|
+
profileId: this.profile.profileId,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async closeOnce() {
|
|
492
|
+
if (this.closed)
|
|
493
|
+
return;
|
|
494
|
+
this.closed = true;
|
|
495
|
+
for (const starting of this.startingBySession.values()) {
|
|
496
|
+
starting.closed = true;
|
|
497
|
+
starting.controller.abort();
|
|
498
|
+
}
|
|
499
|
+
for (const run of [...this.activeBySession.values()])
|
|
500
|
+
run.abortConnection();
|
|
501
|
+
this.pendingApprovals.clear();
|
|
502
|
+
try {
|
|
503
|
+
await this.transport.close();
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
throw mapError(error, this.profile, 'close connection');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
class HermesSession {
|
|
511
|
+
client;
|
|
512
|
+
sessionId;
|
|
513
|
+
state;
|
|
514
|
+
closed = false;
|
|
515
|
+
unsafe = false;
|
|
516
|
+
constructor(client, sessionId, state) {
|
|
517
|
+
this.client = client;
|
|
518
|
+
this.sessionId = sessionId;
|
|
519
|
+
this.state = state;
|
|
520
|
+
}
|
|
521
|
+
ref() {
|
|
522
|
+
return this.client.sessionRef(this.sessionId, this.state);
|
|
523
|
+
}
|
|
524
|
+
capabilities() {
|
|
525
|
+
this.assertOpen();
|
|
526
|
+
return Promise.resolve(this.client.capabilityManifest());
|
|
527
|
+
}
|
|
528
|
+
start(input, options = {}) {
|
|
529
|
+
this.assertOpen();
|
|
530
|
+
return this.client.startRun(this, this.sessionId, this.state, input, options);
|
|
531
|
+
}
|
|
532
|
+
respond(requestId, response) {
|
|
533
|
+
this.assertOpen();
|
|
534
|
+
return this.client.respond(this, this.sessionId, requestId, response);
|
|
535
|
+
}
|
|
536
|
+
close() {
|
|
537
|
+
if (this.closed)
|
|
538
|
+
return Promise.resolve();
|
|
539
|
+
this.closed = true;
|
|
540
|
+
this.client.closeSession(this, this.sessionId);
|
|
541
|
+
return Promise.resolve();
|
|
542
|
+
}
|
|
543
|
+
markUnsafe() {
|
|
544
|
+
this.unsafe = true;
|
|
545
|
+
}
|
|
546
|
+
assertOpen() {
|
|
547
|
+
if (!this.closed && !this.unsafe)
|
|
548
|
+
return;
|
|
549
|
+
throw this.unsafe ? sessionUnsafe(undefined) : sessionClosed();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
class HermesRun {
|
|
553
|
+
reference;
|
|
554
|
+
owner;
|
|
555
|
+
providerRunId;
|
|
556
|
+
transport;
|
|
557
|
+
profile;
|
|
558
|
+
options;
|
|
559
|
+
supportsCancel;
|
|
560
|
+
onMapping;
|
|
561
|
+
onSettling;
|
|
562
|
+
onUnsafe;
|
|
563
|
+
approvalResponseGates = new Set();
|
|
564
|
+
controller = new AbortController();
|
|
565
|
+
eventQueue;
|
|
566
|
+
settlement;
|
|
567
|
+
streamClosed;
|
|
568
|
+
cancelPromise;
|
|
569
|
+
confirmedApprovalChoice;
|
|
570
|
+
finalResult;
|
|
571
|
+
reconcilePromise;
|
|
572
|
+
resolveSettlement;
|
|
573
|
+
resolveStreamClosed;
|
|
574
|
+
sequence = 0;
|
|
575
|
+
terminalSignal;
|
|
576
|
+
timeout;
|
|
577
|
+
constructor(reference, owner, providerRunId, timeoutMs, transport, profile, options, supportsCancel, onMapping, onSettling, onUnsafe) {
|
|
578
|
+
this.reference = reference;
|
|
579
|
+
this.owner = owner;
|
|
580
|
+
this.providerRunId = providerRunId;
|
|
581
|
+
this.transport = transport;
|
|
582
|
+
this.profile = profile;
|
|
583
|
+
this.options = options;
|
|
584
|
+
this.supportsCancel = supportsCancel;
|
|
585
|
+
this.onMapping = onMapping;
|
|
586
|
+
this.onSettling = onSettling;
|
|
587
|
+
this.onUnsafe = onUnsafe;
|
|
588
|
+
this.eventQueue = new EventQueue(options.maxRunEvents);
|
|
589
|
+
this.settlement = new Promise((resolve) => {
|
|
590
|
+
this.resolveSettlement = resolve;
|
|
591
|
+
});
|
|
592
|
+
this.streamClosed = new Promise((resolve) => {
|
|
593
|
+
this.resolveStreamClosed = resolve;
|
|
594
|
+
});
|
|
595
|
+
if (timeoutMs !== undefined) {
|
|
596
|
+
this.timeout = setTimeout(() => {
|
|
597
|
+
if (this.supportsCancel) {
|
|
598
|
+
void this.cancel().catch(() => {
|
|
599
|
+
this.abortConnection();
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
this.abortConnection();
|
|
604
|
+
}
|
|
605
|
+
}, timeoutMs);
|
|
606
|
+
this.timeout.unref();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
open() {
|
|
610
|
+
this.emit({ type: 'run.started', data: {} });
|
|
611
|
+
const iterable = this.transport.subscribe(`${runPath(this.providerRunId)}/events`, { signal: this.controller.signal });
|
|
612
|
+
void this.pump(iterable[Symbol.asyncIterator]()).catch(() => {
|
|
613
|
+
if (!this.isTerminal())
|
|
614
|
+
this.abortConnection();
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
ref() {
|
|
618
|
+
return { ...this.reference };
|
|
619
|
+
}
|
|
620
|
+
events() {
|
|
621
|
+
return this.eventQueue.iterable();
|
|
622
|
+
}
|
|
623
|
+
cancel() {
|
|
624
|
+
if (this.isTerminal())
|
|
625
|
+
return Promise.resolve({ mode: 'already_terminal' });
|
|
626
|
+
if (!this.supportsCancel) {
|
|
627
|
+
return Promise.reject(new HarnessError('unsupported_capability', 'The connected Hermes Agent API Server does not advertise Run stop.', {
|
|
628
|
+
retryable: false,
|
|
629
|
+
providerId: HERMES_PROVIDER_ID,
|
|
630
|
+
profileId: this.profile.profileId,
|
|
631
|
+
details: { capability: 'run.cancel' },
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
this.cancelPromise ??= this.cancelOnce();
|
|
635
|
+
return this.cancelPromise;
|
|
636
|
+
}
|
|
637
|
+
result() {
|
|
638
|
+
return this.settlement;
|
|
639
|
+
}
|
|
640
|
+
isTerminal() {
|
|
641
|
+
return this.finalResult !== undefined;
|
|
642
|
+
}
|
|
643
|
+
hasTerminalBoundary() {
|
|
644
|
+
return this.isTerminal() || this.terminalSignal !== undefined;
|
|
645
|
+
}
|
|
646
|
+
emit(mapped) {
|
|
647
|
+
if (this.isTerminal())
|
|
648
|
+
return;
|
|
649
|
+
if (!this.eventQueue.push(this.portableEvent(mapped)))
|
|
650
|
+
this.abortConnection();
|
|
651
|
+
}
|
|
652
|
+
emitSettlement(mapped) {
|
|
653
|
+
if (this.isTerminal())
|
|
654
|
+
return;
|
|
655
|
+
this.eventQueue.pushTerminal(this.portableEvent(mapped));
|
|
656
|
+
}
|
|
657
|
+
abortConnection() {
|
|
658
|
+
if (this.isTerminal())
|
|
659
|
+
return;
|
|
660
|
+
this.onUnsafe(this);
|
|
661
|
+
this.controller.abort();
|
|
662
|
+
this.finish({ status: 'connection_aborted' }, 'connection.aborted');
|
|
663
|
+
}
|
|
664
|
+
failIncompatible(surface) {
|
|
665
|
+
if (this.isTerminal())
|
|
666
|
+
return;
|
|
667
|
+
this.onUnsafe(this);
|
|
668
|
+
this.controller.abort();
|
|
669
|
+
this.fail(providerIncompatible(this.profile, surface));
|
|
670
|
+
}
|
|
671
|
+
beginApprovalResponse() {
|
|
672
|
+
let resolveGate;
|
|
673
|
+
const gate = new Promise((resolve) => {
|
|
674
|
+
resolveGate = resolve;
|
|
675
|
+
});
|
|
676
|
+
this.approvalResponseGates.add(gate);
|
|
677
|
+
return () => {
|
|
678
|
+
this.approvalResponseGates.delete(gate);
|
|
679
|
+
resolveGate();
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
confirmApproval(choice) {
|
|
683
|
+
if (this.confirmedApprovalChoice !== undefined)
|
|
684
|
+
return false;
|
|
685
|
+
this.confirmedApprovalChoice = choice;
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
acceptConfirmedApproval(choice) {
|
|
689
|
+
if (this.confirmedApprovalChoice !== choice)
|
|
690
|
+
return false;
|
|
691
|
+
this.confirmedApprovalChoice = undefined;
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
async pump(iterator) {
|
|
695
|
+
try {
|
|
696
|
+
for (;;) {
|
|
697
|
+
const next = await iterator.next();
|
|
698
|
+
if (next.done)
|
|
699
|
+
break;
|
|
700
|
+
validateSseDispatch(next.value);
|
|
701
|
+
const value = parseSseJson(next.value.data);
|
|
702
|
+
const mapping = mapHermesEvent(value, this.providerRunId);
|
|
703
|
+
if (mapping.kind === 'terminal') {
|
|
704
|
+
this.handleTerminalSignal(mapping.eventType);
|
|
705
|
+
}
|
|
706
|
+
else if (this.terminalSignal !== undefined &&
|
|
707
|
+
mapping.kind !== 'subagent' &&
|
|
708
|
+
mapping.kind !== 'provider') {
|
|
709
|
+
this.failIncompatible('Run event after terminal evidence');
|
|
710
|
+
}
|
|
711
|
+
else if (mapping.kind === 'portable') {
|
|
712
|
+
this.emit(mapping.event);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
this.onMapping(this, mapping);
|
|
716
|
+
}
|
|
717
|
+
if (this.finalResult !== undefined &&
|
|
718
|
+
this.finalResult.status !== 'completed') {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!this.isTerminal() && this.terminalSignal === undefined) {
|
|
723
|
+
await this.reconcileAfterStreamLoss();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
if (this.isTerminal())
|
|
728
|
+
return;
|
|
729
|
+
if (this.terminalSignal !== undefined)
|
|
730
|
+
return;
|
|
731
|
+
const mapped = mapError(error, this.profile, 'consume Run events');
|
|
732
|
+
if (error instanceof HttpTransportError &&
|
|
733
|
+
uncertainTransportCodes.has(error.code)) {
|
|
734
|
+
await this.reconcileAfterStreamLoss();
|
|
735
|
+
}
|
|
736
|
+
else if (mapped.code === 'provider_api_incompatible') {
|
|
737
|
+
this.onUnsafe(this);
|
|
738
|
+
this.fail(mapped);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
this.abortConnection();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
await iterator.return?.().catch(() => undefined);
|
|
746
|
+
this.resolveStreamClosed();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
handleTerminalSignal(eventType) {
|
|
750
|
+
if (this.terminalSignal !== undefined) {
|
|
751
|
+
this.failIncompatible('duplicate Run terminal event');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (this.isTerminal())
|
|
755
|
+
return;
|
|
756
|
+
this.terminalSignal = eventType;
|
|
757
|
+
this.reconcilePromise ??= this.reconcileTerminal(eventType);
|
|
758
|
+
void this.reconcilePromise.catch(() => undefined);
|
|
759
|
+
}
|
|
760
|
+
async reconcileAfterStreamLoss() {
|
|
761
|
+
this.reconcilePromise ??= this.reconcileTerminal(this.terminalSignal);
|
|
762
|
+
await this.reconcilePromise;
|
|
763
|
+
}
|
|
764
|
+
async reconcileTerminal(expectedEvent) {
|
|
765
|
+
const deadline = Date.now() + this.options.reconcileTimeoutMs;
|
|
766
|
+
for (;;) {
|
|
767
|
+
if (this.isTerminal())
|
|
768
|
+
return;
|
|
769
|
+
let status;
|
|
770
|
+
try {
|
|
771
|
+
status = parseHermesRunStatus(await requestProviderJson(this.transport, runPath(this.providerRunId), {}, this.profile, 'reconcile Run status', 'run'), this.providerRunId, this.reference.sessionId);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
const mapped = mapError(error, this.profile, 'reconcile Run status');
|
|
775
|
+
if (mapped.code === 'provider_api_incompatible') {
|
|
776
|
+
this.onUnsafe(this);
|
|
777
|
+
this.controller.abort();
|
|
778
|
+
this.fail(mapped);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
this.abortConnection();
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (status.result !== undefined &&
|
|
786
|
+
status.terminalEventType !== undefined) {
|
|
787
|
+
if (expectedEvent !== undefined &&
|
|
788
|
+
expectedEvent !== status.terminalEventType) {
|
|
789
|
+
this.failIncompatible('contradictory Run terminal evidence');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
while (this.approvalResponseGates.size > 0) {
|
|
793
|
+
await Promise.all([...this.approvalResponseGates]);
|
|
794
|
+
}
|
|
795
|
+
if (this.isTerminal())
|
|
796
|
+
return;
|
|
797
|
+
if (status.result.status === 'completed') {
|
|
798
|
+
if (status.result.finalMessage !== undefined) {
|
|
799
|
+
this.emit({
|
|
800
|
+
type: 'message.completed',
|
|
801
|
+
data: { message: status.result.finalMessage },
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (status.result.usage !== undefined) {
|
|
805
|
+
this.emit({ type: 'usage.updated', data: status.result.usage });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (expectedEvent !== undefined) {
|
|
809
|
+
await Promise.race([
|
|
810
|
+
this.streamClosed,
|
|
811
|
+
boundedDelay(this.options.lateEventDrainTimeoutMs),
|
|
812
|
+
]);
|
|
813
|
+
if (this.isTerminal())
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
while (this.approvalResponseGates.size > 0) {
|
|
817
|
+
await Promise.all([...this.approvalResponseGates]);
|
|
818
|
+
}
|
|
819
|
+
if (this.isTerminal())
|
|
820
|
+
return;
|
|
821
|
+
this.finish(status.result, terminalEventType(status.result.status));
|
|
822
|
+
this.controller.abort();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (Date.now() >= deadline) {
|
|
826
|
+
this.abortConnection();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
await boundedDelay(this.options.reconcilePollIntervalMs);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async cancelOnce() {
|
|
833
|
+
try {
|
|
834
|
+
parseHermesStopResponse(await requestProviderJson(this.transport, `${runPath(this.providerRunId)}/stop`, { method: 'POST' }, this.profile, 'stop Run', 'run'), this.providerRunId);
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
const mapped = mapError(error, this.profile, 'stop Run');
|
|
838
|
+
if (uncertainMutationFailure(mapped))
|
|
839
|
+
this.abortConnection();
|
|
840
|
+
throw mapped;
|
|
841
|
+
}
|
|
842
|
+
const outcome = await Promise.race([
|
|
843
|
+
this.settlement,
|
|
844
|
+
boundedDelay(this.options.cancelSettlementTimeoutMs).then(() => undefined),
|
|
845
|
+
]);
|
|
846
|
+
if (outcome === undefined) {
|
|
847
|
+
this.abortConnection();
|
|
848
|
+
return { mode: 'connection_aborted' };
|
|
849
|
+
}
|
|
850
|
+
if (outcome.status === 'cancelled')
|
|
851
|
+
return { mode: 'native' };
|
|
852
|
+
if (outcome.status === 'connection_aborted') {
|
|
853
|
+
return { mode: 'connection_aborted' };
|
|
854
|
+
}
|
|
855
|
+
return { mode: 'already_terminal' };
|
|
856
|
+
}
|
|
857
|
+
fail(error) {
|
|
858
|
+
this.finish({
|
|
859
|
+
status: 'failed',
|
|
860
|
+
providerResult: {
|
|
861
|
+
error: error.code,
|
|
862
|
+
...(error.providerCode === undefined
|
|
863
|
+
? {}
|
|
864
|
+
: { providerCode: error.providerCode }),
|
|
865
|
+
},
|
|
866
|
+
}, 'run.failed');
|
|
867
|
+
}
|
|
868
|
+
finish(result, type) {
|
|
869
|
+
if (this.isTerminal())
|
|
870
|
+
return;
|
|
871
|
+
this.onSettling(this);
|
|
872
|
+
this.finalResult = result;
|
|
873
|
+
if (this.timeout !== undefined)
|
|
874
|
+
clearTimeout(this.timeout);
|
|
875
|
+
this.eventQueue.pushTerminal(this.portableEvent({ type, data: result }));
|
|
876
|
+
this.eventQueue.close();
|
|
877
|
+
this.resolveSettlement(result);
|
|
878
|
+
}
|
|
879
|
+
portableEvent(mapped) {
|
|
880
|
+
const sequence = this.sequence++;
|
|
881
|
+
return {
|
|
882
|
+
id: `${this.reference.runId}:event:${String(sequence)}`,
|
|
883
|
+
type: mapped.type,
|
|
884
|
+
providerId: this.reference.providerId,
|
|
885
|
+
profileId: this.reference.profileId,
|
|
886
|
+
sessionId: this.reference.sessionId,
|
|
887
|
+
runId: this.reference.runId,
|
|
888
|
+
sequence,
|
|
889
|
+
timestamp: new Date().toISOString(),
|
|
890
|
+
data: mapped.data,
|
|
891
|
+
...(mapped.providerEventType === undefined
|
|
892
|
+
? {}
|
|
893
|
+
: { providerEventType: mapped.providerEventType }),
|
|
894
|
+
...(mapped.raw === undefined ? {} : { raw: mapped.raw }),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
class EventQueue {
|
|
899
|
+
capacity;
|
|
900
|
+
values = [];
|
|
901
|
+
closed = false;
|
|
902
|
+
consumed = false;
|
|
903
|
+
waiter;
|
|
904
|
+
constructor(capacity) {
|
|
905
|
+
this.capacity = capacity;
|
|
906
|
+
}
|
|
907
|
+
push(event) {
|
|
908
|
+
if (this.closed)
|
|
909
|
+
return false;
|
|
910
|
+
if (this.waiter !== undefined) {
|
|
911
|
+
const waiter = this.waiter;
|
|
912
|
+
this.waiter = undefined;
|
|
913
|
+
waiter({ done: false, value: event });
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
if (this.values.length >= this.capacity - 1)
|
|
917
|
+
return false;
|
|
918
|
+
this.values.push(event);
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
pushTerminal(event) {
|
|
922
|
+
if (this.waiter !== undefined) {
|
|
923
|
+
const waiter = this.waiter;
|
|
924
|
+
this.waiter = undefined;
|
|
925
|
+
waiter({ done: false, value: event });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
this.values.push(event);
|
|
929
|
+
}
|
|
930
|
+
close() {
|
|
931
|
+
this.closed = true;
|
|
932
|
+
}
|
|
933
|
+
iterable() {
|
|
934
|
+
if (this.consumed) {
|
|
935
|
+
return {
|
|
936
|
+
[Symbol.asyncIterator]: () => ({
|
|
937
|
+
next: () => Promise.reject(new HarnessError('run_conflict', 'Hermes Agent Run events already have a consumer.', { retryable: false, providerId: HERMES_PROVIDER_ID })),
|
|
938
|
+
}),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
this.consumed = true;
|
|
942
|
+
return {
|
|
943
|
+
[Symbol.asyncIterator]: () => ({ next: () => this.next() }),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
next() {
|
|
947
|
+
const event = this.values.shift();
|
|
948
|
+
if (event !== undefined)
|
|
949
|
+
return Promise.resolve({ done: false, value: event });
|
|
950
|
+
if (this.closed)
|
|
951
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
952
|
+
if (this.waiter !== undefined) {
|
|
953
|
+
return Promise.reject(new HarnessError('run_conflict', 'A Hermes Agent event read is already pending.', { retryable: false, providerId: HERMES_PROVIDER_ID }));
|
|
954
|
+
}
|
|
955
|
+
return new Promise((resolve) => {
|
|
956
|
+
this.waiter = resolve;
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const jsonHeaders = { 'content-type': 'application/json' };
|
|
961
|
+
function hermesCapabilityManifest(profile, runtimeIdentity, observed) {
|
|
962
|
+
const native = { mode: 'native', source: 'handshake' };
|
|
963
|
+
return {
|
|
964
|
+
providerId: HERMES_PROVIDER_ID,
|
|
965
|
+
profileId: profile.profileId,
|
|
966
|
+
observedAt: new Date().toISOString(),
|
|
967
|
+
runtimeIdentity,
|
|
968
|
+
capabilities: {
|
|
969
|
+
'session.create': native,
|
|
970
|
+
'session.resume': native,
|
|
971
|
+
'session.close': {
|
|
972
|
+
mode: 'adapter_controlled',
|
|
973
|
+
source: 'configuration',
|
|
974
|
+
reason: 'Portable close releases only the local Session handle.',
|
|
975
|
+
},
|
|
976
|
+
'session.workspace': {
|
|
977
|
+
mode: 'unsupported',
|
|
978
|
+
source: 'handshake',
|
|
979
|
+
},
|
|
980
|
+
'run.stream': native,
|
|
981
|
+
'run.cancel': observed.features.cancel
|
|
982
|
+
? native
|
|
983
|
+
: { mode: 'unsupported', source: 'handshake' },
|
|
984
|
+
'run.timeout': observed.features.cancel
|
|
985
|
+
? {
|
|
986
|
+
mode: 'emulated',
|
|
987
|
+
source: 'configuration',
|
|
988
|
+
reason: 'A local timer invokes the documented Run stop endpoint.',
|
|
989
|
+
}
|
|
990
|
+
: {
|
|
991
|
+
mode: 'adapter_controlled',
|
|
992
|
+
source: 'configuration',
|
|
993
|
+
reason: 'A local timer can only abort the Adapter connection.',
|
|
994
|
+
},
|
|
995
|
+
'run.concurrent': {
|
|
996
|
+
mode: 'unsupported',
|
|
997
|
+
source: 'configuration',
|
|
998
|
+
limits: { perSession: 1 },
|
|
999
|
+
},
|
|
1000
|
+
'interaction.approval': observed.features.approval
|
|
1001
|
+
? native
|
|
1002
|
+
: { mode: 'unsupported', source: 'handshake' },
|
|
1003
|
+
'input.text': native,
|
|
1004
|
+
'input.file': { mode: 'unsupported', source: 'schema' },
|
|
1005
|
+
'input.image': { mode: 'unsupported', source: 'schema' },
|
|
1006
|
+
'unknown_event.raw': {
|
|
1007
|
+
mode: 'adapter_controlled',
|
|
1008
|
+
source: 'configuration',
|
|
1009
|
+
},
|
|
1010
|
+
[`${HERMES_SUBAGENT_EXTENSION}.observe`]: {
|
|
1011
|
+
mode: 'unknown',
|
|
1012
|
+
source: 'schema',
|
|
1013
|
+
reason: 'The current capability document does not advertise child-Session events.',
|
|
1014
|
+
},
|
|
1015
|
+
'native.client': native,
|
|
1016
|
+
},
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function connectionOptions(value) {
|
|
1020
|
+
const options = value === undefined ? {} : runtimeRecord(value);
|
|
1021
|
+
if (options === undefined) {
|
|
1022
|
+
throw profileInvalid('Hermes Agent providerOptions must be an object.');
|
|
1023
|
+
}
|
|
1024
|
+
const allowed = new Set([
|
|
1025
|
+
'cancelSettlementTimeoutMs',
|
|
1026
|
+
'lateEventDrainTimeoutMs',
|
|
1027
|
+
'maxRunEvents',
|
|
1028
|
+
'reconcilePollIntervalMs',
|
|
1029
|
+
'reconcileTimeoutMs',
|
|
1030
|
+
'requestTimeoutMs',
|
|
1031
|
+
'sseConnectTimeoutMs',
|
|
1032
|
+
]);
|
|
1033
|
+
const unknown = Object.keys(options).find((name) => !allowed.has(name));
|
|
1034
|
+
if (unknown !== undefined) {
|
|
1035
|
+
throw profileInvalid(`Hermes Agent Profile option ${unknown} is unknown.`);
|
|
1036
|
+
}
|
|
1037
|
+
const maxRunEvents = profileInteger(options['maxRunEvents'], defaultMaxRunEvents, 'maxRunEvents');
|
|
1038
|
+
if (maxRunEvents < 2 || maxRunEvents > maximumRunEventCapacity) {
|
|
1039
|
+
throw profileInvalid('Hermes Agent maxRunEvents must be between 2 and the supported upper bound.');
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
maxRunEvents,
|
|
1043
|
+
cancelSettlementTimeoutMs: profileTimer(options['cancelSettlementTimeoutMs'], defaultCancelSettlementTimeoutMs, 'cancelSettlementTimeoutMs'),
|
|
1044
|
+
lateEventDrainTimeoutMs: profileTimer(options['lateEventDrainTimeoutMs'], defaultLateEventDrainTimeoutMs, 'lateEventDrainTimeoutMs'),
|
|
1045
|
+
reconcilePollIntervalMs: profileTimer(options['reconcilePollIntervalMs'], defaultReconcilePollIntervalMs, 'reconcilePollIntervalMs'),
|
|
1046
|
+
reconcileTimeoutMs: profileTimer(options['reconcileTimeoutMs'], defaultReconcileTimeoutMs, 'reconcileTimeoutMs'),
|
|
1047
|
+
...(options['requestTimeoutMs'] === undefined
|
|
1048
|
+
? {}
|
|
1049
|
+
: {
|
|
1050
|
+
requestTimeoutMs: profileTimer(options['requestTimeoutMs'], 0, 'requestTimeoutMs'),
|
|
1051
|
+
}),
|
|
1052
|
+
...(options['sseConnectTimeoutMs'] === undefined
|
|
1053
|
+
? {}
|
|
1054
|
+
: {
|
|
1055
|
+
sseConnectTimeoutMs: profileTimer(options['sseConnectTimeoutMs'], 0, 'sseConnectTimeoutMs'),
|
|
1056
|
+
}),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function validateProfile(profile, factoryOptions) {
|
|
1060
|
+
if (profile.providerId !== HERMES_PROVIDER_ID ||
|
|
1061
|
+
profile.connection.kind !== 'endpoint' ||
|
|
1062
|
+
(profile.connection.transport !== undefined &&
|
|
1063
|
+
profile.connection.transport !== 'http') ||
|
|
1064
|
+
profile.connection.url.length === 0) {
|
|
1065
|
+
throw profileInvalid('Hermes Agent requires an HTTP endpoint Profile owned by the host or an external service.', profile);
|
|
1066
|
+
}
|
|
1067
|
+
let endpoint;
|
|
1068
|
+
try {
|
|
1069
|
+
endpoint = new URL(profile.connection.url);
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
throw profileInvalid('Hermes Agent endpoint URL must be absolute.', profile);
|
|
1073
|
+
}
|
|
1074
|
+
if (endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') {
|
|
1075
|
+
throw profileInvalid('Hermes Agent endpoint URL must use HTTP or HTTPS.', profile);
|
|
1076
|
+
}
|
|
1077
|
+
if (profile.connection.authRef !== undefined &&
|
|
1078
|
+
factoryOptions.resolveAuthHeaders === undefined) {
|
|
1079
|
+
throw profileInvalid('Hermes Agent authRef requires a host-provided authentication-header resolver.', profile);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async function resolveHeaders(profile, factoryOptions) {
|
|
1083
|
+
if (profile.connection.kind !== 'endpoint') {
|
|
1084
|
+
throw profileInvalid(undefined, profile);
|
|
1085
|
+
}
|
|
1086
|
+
const reference = profile.connection.authRef;
|
|
1087
|
+
if (reference === undefined)
|
|
1088
|
+
return undefined;
|
|
1089
|
+
try {
|
|
1090
|
+
const headers = await factoryOptions.resolveAuthHeaders?.(reference);
|
|
1091
|
+
if (headers === undefined ||
|
|
1092
|
+
runtimeRecord(headers) === undefined ||
|
|
1093
|
+
Object.values(headers).some((value) => typeof value !== 'string')) {
|
|
1094
|
+
throw new TypeError('invalid authentication headers');
|
|
1095
|
+
}
|
|
1096
|
+
return headers;
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
throw new HarnessError('authentication_failed', 'The host could not resolve Hermes Agent authentication headers.', {
|
|
1100
|
+
retryable: false,
|
|
1101
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1102
|
+
profileId: profile.profileId,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function requestProviderJson(transport, path, options, profile, phase, notFound, connecting = false) {
|
|
1107
|
+
let response;
|
|
1108
|
+
try {
|
|
1109
|
+
response = await transport.request(path, options);
|
|
1110
|
+
}
|
|
1111
|
+
catch (error) {
|
|
1112
|
+
throw mapError(error, profile, phase, connecting);
|
|
1113
|
+
}
|
|
1114
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1115
|
+
throw responseError(response.status, profile, phase, notFound);
|
|
1116
|
+
}
|
|
1117
|
+
return parseJsonResponse(response, phase);
|
|
1118
|
+
}
|
|
1119
|
+
function parseJsonResponse(response, phase) {
|
|
1120
|
+
if (!response.contentType?.toLowerCase().startsWith('application/json')) {
|
|
1121
|
+
throw providerIncompatible(undefined, `${phase} Content-Type`);
|
|
1122
|
+
}
|
|
1123
|
+
let text;
|
|
1124
|
+
try {
|
|
1125
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(response.body);
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
throw providerIncompatible(undefined, `${phase} encoding`);
|
|
1129
|
+
}
|
|
1130
|
+
try {
|
|
1131
|
+
return JSON.parse(text);
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
throw providerIncompatible(undefined, `${phase} JSON`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function parseSseJson(data) {
|
|
1138
|
+
try {
|
|
1139
|
+
return JSON.parse(data);
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
throw providerIncompatible(undefined, 'Run event JSON');
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function responseError(status, profile, phase, notFound) {
|
|
1146
|
+
if (status === 401 || status === 403) {
|
|
1147
|
+
return new HarnessError('authentication_failed', `Hermes Agent rejected ${phase} authentication.`, {
|
|
1148
|
+
retryable: false,
|
|
1149
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1150
|
+
profileId: profile.profileId,
|
|
1151
|
+
providerCode: String(status),
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
if (status === 404) {
|
|
1155
|
+
const code = notFound === 'session'
|
|
1156
|
+
? 'session_not_found'
|
|
1157
|
+
: notFound === 'compatibility'
|
|
1158
|
+
? 'provider_api_incompatible'
|
|
1159
|
+
: notFound === 'interaction'
|
|
1160
|
+
? 'invalid_request'
|
|
1161
|
+
: 'provider_error';
|
|
1162
|
+
return new HarnessError(code, `Hermes Agent could not find the resource for ${phase}.`, {
|
|
1163
|
+
retryable: false,
|
|
1164
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1165
|
+
profileId: profile.profileId,
|
|
1166
|
+
providerCode: String(status),
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
const code = status === 400 || status === 422
|
|
1170
|
+
? 'invalid_request'
|
|
1171
|
+
: status === 409
|
|
1172
|
+
? 'run_conflict'
|
|
1173
|
+
: 'provider_error';
|
|
1174
|
+
return new HarnessError(code, `Hermes Agent rejected ${phase}.`, {
|
|
1175
|
+
retryable: status === 429 || status >= 500,
|
|
1176
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1177
|
+
profileId: profile.profileId,
|
|
1178
|
+
providerCode: String(status),
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
function mapError(error, profile, phase, connecting = false) {
|
|
1182
|
+
if (error instanceof HarnessError)
|
|
1183
|
+
return error;
|
|
1184
|
+
if (error instanceof HttpTransportError) {
|
|
1185
|
+
if (error.code === 'http_status' && error.status !== undefined) {
|
|
1186
|
+
return responseError(error.status, profile, phase, 'compatibility');
|
|
1187
|
+
}
|
|
1188
|
+
const code = error.code === 'request_timeout'
|
|
1189
|
+
? 'timeout'
|
|
1190
|
+
: error.code === 'transport_closed'
|
|
1191
|
+
? 'connection_aborted'
|
|
1192
|
+
: connecting && error.code === 'network_failure'
|
|
1193
|
+
? 'connection_failed'
|
|
1194
|
+
: 'provider_error';
|
|
1195
|
+
return new HarnessError(code, `Hermes Agent ${phase} did not complete.`, {
|
|
1196
|
+
retryable: error.code === 'request_timeout' ||
|
|
1197
|
+
error.code === 'capacity_exceeded' ||
|
|
1198
|
+
error.code === 'network_failure',
|
|
1199
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1200
|
+
profileId: profile.profileId,
|
|
1201
|
+
providerCode: error.code,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
return new HarnessError(connecting ? 'connection_failed' : 'provider_error', `Hermes Agent ${phase} failed.`, {
|
|
1205
|
+
retryable: false,
|
|
1206
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1207
|
+
profileId: profile.profileId,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function uncertainRequestFailure(error) {
|
|
1211
|
+
return (error.code === 'timeout' ||
|
|
1212
|
+
(error.providerCode !== undefined &&
|
|
1213
|
+
uncertainTransportCodes.has(error.providerCode)));
|
|
1214
|
+
}
|
|
1215
|
+
function uncertainMutationFailure(error) {
|
|
1216
|
+
if (uncertainRequestFailure(error) ||
|
|
1217
|
+
error.code === 'provider_api_incompatible') {
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
if (error.code !== 'provider_error')
|
|
1221
|
+
return false;
|
|
1222
|
+
if (error.providerCode === undefined)
|
|
1223
|
+
return true;
|
|
1224
|
+
const status = Number(error.providerCode);
|
|
1225
|
+
return !Number.isInteger(status) || status >= 500;
|
|
1226
|
+
}
|
|
1227
|
+
function approvalChoice(response) {
|
|
1228
|
+
if (response.kind === 'provider') {
|
|
1229
|
+
const options = runtimeRecord(response.value);
|
|
1230
|
+
if (options !== undefined && Object.keys(options).length === 1) {
|
|
1231
|
+
const choice = options['choice'];
|
|
1232
|
+
if (choice === 'once' ||
|
|
1233
|
+
choice === 'session' ||
|
|
1234
|
+
choice === 'always' ||
|
|
1235
|
+
choice === 'deny') {
|
|
1236
|
+
return choice;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
throw invalidInteraction();
|
|
1240
|
+
}
|
|
1241
|
+
if (response.kind !== 'approval')
|
|
1242
|
+
throw invalidInteraction();
|
|
1243
|
+
if (response.decision === 'deny')
|
|
1244
|
+
return 'deny';
|
|
1245
|
+
if (response.providerOptions === undefined)
|
|
1246
|
+
return 'once';
|
|
1247
|
+
const options = runtimeRecord(response.providerOptions);
|
|
1248
|
+
if (options !== undefined &&
|
|
1249
|
+
Object.keys(options).length === 1 &&
|
|
1250
|
+
(options['scope'] === 'session' || options['scope'] === 'always')) {
|
|
1251
|
+
return options['scope'];
|
|
1252
|
+
}
|
|
1253
|
+
throw invalidInteraction();
|
|
1254
|
+
}
|
|
1255
|
+
function validateSseDispatch(event) {
|
|
1256
|
+
if (event.event !== undefined && event.event !== 'message') {
|
|
1257
|
+
throw providerIncompatible(undefined, 'SSE dispatch type');
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function terminalEventType(status) {
|
|
1261
|
+
return status === 'completed'
|
|
1262
|
+
? 'run.completed'
|
|
1263
|
+
: status === 'cancelled'
|
|
1264
|
+
? 'run.cancelled'
|
|
1265
|
+
: status === 'connection_aborted'
|
|
1266
|
+
? 'connection.aborted'
|
|
1267
|
+
: 'run.failed';
|
|
1268
|
+
}
|
|
1269
|
+
function sessionPath(sessionId) {
|
|
1270
|
+
return `api/sessions/${encodeURIComponent(sessionId)}`;
|
|
1271
|
+
}
|
|
1272
|
+
function runPath(providerRunId) {
|
|
1273
|
+
return `v1/runs/${encodeURIComponent(providerRunId)}`;
|
|
1274
|
+
}
|
|
1275
|
+
function snapshotProfile(profile) {
|
|
1276
|
+
return structuredClone(profile);
|
|
1277
|
+
}
|
|
1278
|
+
function endpointUrl(profile) {
|
|
1279
|
+
if (profile.connection.kind !== 'endpoint')
|
|
1280
|
+
throw profileInvalid();
|
|
1281
|
+
return profile.connection.url;
|
|
1282
|
+
}
|
|
1283
|
+
function profileInteger(value, fallback, label) {
|
|
1284
|
+
if (value === undefined)
|
|
1285
|
+
return fallback;
|
|
1286
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
|
|
1287
|
+
throw profileInvalid(`Hermes Agent ${label} must be a positive integer.`);
|
|
1288
|
+
}
|
|
1289
|
+
return value;
|
|
1290
|
+
}
|
|
1291
|
+
function profileTimer(value, fallback, label) {
|
|
1292
|
+
const timer = profileInteger(value, fallback, label);
|
|
1293
|
+
if (timer > maximumTimerMilliseconds) {
|
|
1294
|
+
throw profileInvalid(`Hermes Agent ${label} exceeds the supported timer range.`);
|
|
1295
|
+
}
|
|
1296
|
+
return timer;
|
|
1297
|
+
}
|
|
1298
|
+
function validateRunTimeout(value) {
|
|
1299
|
+
if (!Number.isSafeInteger(value) ||
|
|
1300
|
+
value <= 0 ||
|
|
1301
|
+
value > maximumTimerMilliseconds) {
|
|
1302
|
+
throw new HarnessError('invalid_request', 'Hermes Agent Run timeout must be a positive supported timer.', { retryable: false, providerId: HERMES_PROVIDER_ID });
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function boundedDelay(milliseconds) {
|
|
1306
|
+
return new Promise((resolve) => {
|
|
1307
|
+
const timer = setTimeout(resolve, milliseconds);
|
|
1308
|
+
timer.unref();
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
function jsonBody(value) {
|
|
1312
|
+
try {
|
|
1313
|
+
const body = JSON.stringify(value);
|
|
1314
|
+
if (typeof body !== 'string')
|
|
1315
|
+
throw new TypeError('undefined JSON');
|
|
1316
|
+
return body;
|
|
1317
|
+
}
|
|
1318
|
+
catch {
|
|
1319
|
+
throw new HarnessError('invalid_request', 'Hermes Agent request data must be JSON serializable.', { retryable: false, providerId: HERMES_PROVIDER_ID });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function runtimeRecord(value) {
|
|
1323
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1324
|
+
? value
|
|
1325
|
+
: undefined;
|
|
1326
|
+
}
|
|
1327
|
+
function profileInvalid(message = 'Hermes Agent Profile is invalid.', profile) {
|
|
1328
|
+
return new HarnessError('profile_invalid', message, {
|
|
1329
|
+
retryable: false,
|
|
1330
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1331
|
+
...(profile === undefined ? {} : { profileId: profile.profileId }),
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
function providerIncompatible(profile, surface) {
|
|
1335
|
+
return new HarnessError('provider_api_incompatible', `Hermes Agent returned an incompatible ${surface}.`, {
|
|
1336
|
+
retryable: false,
|
|
1337
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1338
|
+
...(profile === undefined ? {} : { profileId: profile.profileId }),
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
function invalidInteraction(profile) {
|
|
1342
|
+
return new HarnessError('invalid_request', 'Hermes Agent interaction response is invalid or no longer pending.', {
|
|
1343
|
+
retryable: false,
|
|
1344
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1345
|
+
...(profile === undefined ? {} : { profileId: profile.profileId }),
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
function sessionMismatch(profile) {
|
|
1349
|
+
return new HarnessError('session_provider_mismatch', 'Hermes Agent returned a Session that does not match its reference.', {
|
|
1350
|
+
retryable: false,
|
|
1351
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1352
|
+
profileId: profile.profileId,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
function sessionUnsafe(profile) {
|
|
1356
|
+
return new HarnessError('connection_aborted', 'Hermes Agent Session settlement is uncertain and requires explicit recovery.', {
|
|
1357
|
+
retryable: false,
|
|
1358
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1359
|
+
...(profile === undefined ? {} : { profileId: profile.profileId }),
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function sessionClosed() {
|
|
1363
|
+
return new HarnessError('connection_aborted', 'Hermes Agent Session is closed.', {
|
|
1364
|
+
retryable: false,
|
|
1365
|
+
providerId: HERMES_PROVIDER_ID,
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
//# sourceMappingURL=adapter.js.map
|