@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/protocol.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { HarnessError, providerId, } from '@harapter/core';
|
|
3
|
+
/** Stable Provider identity for the Hermes Agent API Server Adapter. */
|
|
4
|
+
export const HERMES_PROVIDER_ID = providerId('nous.hermes-agent');
|
|
5
|
+
/** Current public API Server compatibility family. */
|
|
6
|
+
export const HERMES_SESSION_COMPATIBILITY_REF = `${HERMES_PROVIDER_ID};api-server=current`;
|
|
7
|
+
/** Typed extension name for Hermes background child-session observations. */
|
|
8
|
+
export const HERMES_SUBAGENT_EXTENSION = 'nous.hermes-agent.subagents';
|
|
9
|
+
const requiredEndpoints = {
|
|
10
|
+
runs: { method: 'POST', path: '/v1/runs' },
|
|
11
|
+
run_status: { method: 'GET', path: '/v1/runs/{run_id}' },
|
|
12
|
+
run_events: { method: 'GET', path: '/v1/runs/{run_id}/events' },
|
|
13
|
+
session_create: { method: 'POST', path: '/api/sessions' },
|
|
14
|
+
session: { method: 'GET', path: '/api/sessions/{session_id}' },
|
|
15
|
+
};
|
|
16
|
+
const optionalEndpoints = {
|
|
17
|
+
run_approval: { method: 'POST', path: '/v1/runs/{run_id}/approval' },
|
|
18
|
+
run_stop: { method: 'POST', path: '/v1/runs/{run_id}/stop' },
|
|
19
|
+
};
|
|
20
|
+
const maximumIdentifierLength = 256;
|
|
21
|
+
const maximumTextLength = 256 * 1024;
|
|
22
|
+
const maximumOptionDepth = 5;
|
|
23
|
+
const maximumOptionNodes = 256;
|
|
24
|
+
const maximumRawDepth = 5;
|
|
25
|
+
const maximumRawNodes = 128;
|
|
26
|
+
const maximumRawArrayItems = 16;
|
|
27
|
+
const maximumRawObjectFields = 32;
|
|
28
|
+
const approvalChoices = new Set(['once', 'session', 'always', 'deny']);
|
|
29
|
+
const sensitiveOptionWords = new Set([
|
|
30
|
+
'authorization',
|
|
31
|
+
'cookie',
|
|
32
|
+
'credential',
|
|
33
|
+
'key',
|
|
34
|
+
'passphrase',
|
|
35
|
+
'password',
|
|
36
|
+
'pem',
|
|
37
|
+
'secret',
|
|
38
|
+
'token',
|
|
39
|
+
]);
|
|
40
|
+
const sensitiveOptionSuffix = /(?:authorization|cookie|credential|passphrase|password|pem|secret|token)$/iu;
|
|
41
|
+
const sensitiveCompactKeySuffix = /(?:api|auth|client|consumer|encryption|private|public|secret|serviceaccount|session|signing)key$/iu;
|
|
42
|
+
const safeRawKeys = new Set([
|
|
43
|
+
'api_calls',
|
|
44
|
+
'child_session_id',
|
|
45
|
+
'choice',
|
|
46
|
+
'choices',
|
|
47
|
+
'command',
|
|
48
|
+
'cost_usd',
|
|
49
|
+
'delta',
|
|
50
|
+
'depth',
|
|
51
|
+
'description',
|
|
52
|
+
'duration',
|
|
53
|
+
'duration_seconds',
|
|
54
|
+
'error',
|
|
55
|
+
'event',
|
|
56
|
+
'files_read',
|
|
57
|
+
'files_written',
|
|
58
|
+
'goal',
|
|
59
|
+
'input_tokens',
|
|
60
|
+
'last_event',
|
|
61
|
+
'model',
|
|
62
|
+
'object',
|
|
63
|
+
'output',
|
|
64
|
+
'output_tail',
|
|
65
|
+
'output_tokens',
|
|
66
|
+
'parent_id',
|
|
67
|
+
'preview',
|
|
68
|
+
'reasoning_tokens',
|
|
69
|
+
'resolved',
|
|
70
|
+
'run_id',
|
|
71
|
+
'session_id',
|
|
72
|
+
'status',
|
|
73
|
+
'subagent_id',
|
|
74
|
+
'summary',
|
|
75
|
+
'text',
|
|
76
|
+
'timestamp',
|
|
77
|
+
'tool',
|
|
78
|
+
'tool_count',
|
|
79
|
+
'total_tokens',
|
|
80
|
+
'usage',
|
|
81
|
+
]);
|
|
82
|
+
/** Validate the capability and route surface needed by the Adapter. */
|
|
83
|
+
export function parseHermesCapabilities(value) {
|
|
84
|
+
const document = requiredRecord(value, 'capability document');
|
|
85
|
+
if (document['object'] !== 'hermes.api_server.capabilities' ||
|
|
86
|
+
document['platform'] !== 'hermes-agent') {
|
|
87
|
+
throw incompatible('capability identity');
|
|
88
|
+
}
|
|
89
|
+
const model = boundedString(document['model'], 'capability model');
|
|
90
|
+
const auth = requiredRecord(document['auth'], 'capability authentication');
|
|
91
|
+
if (auth['type'] !== 'bearer' || typeof auth['required'] !== 'boolean') {
|
|
92
|
+
throw incompatible('capability authentication');
|
|
93
|
+
}
|
|
94
|
+
const features = requiredRecord(document['features'], 'capability features');
|
|
95
|
+
const endpoints = requiredRecord(document['endpoints'], 'capability endpoints');
|
|
96
|
+
for (const [name, expected] of Object.entries(requiredEndpoints)) {
|
|
97
|
+
assertEndpoint(endpoints[name], expected.method, expected.path, name);
|
|
98
|
+
}
|
|
99
|
+
for (const name of [
|
|
100
|
+
'run_submission',
|
|
101
|
+
'run_status',
|
|
102
|
+
'run_events_sse',
|
|
103
|
+
'session_resources',
|
|
104
|
+
]) {
|
|
105
|
+
if (features[name] !== true)
|
|
106
|
+
throw incompatible(`capability ${name}`);
|
|
107
|
+
}
|
|
108
|
+
const cancel = features['run_stop'] === true &&
|
|
109
|
+
endpointMatches(endpoints['run_stop'], optionalEndpoints.run_stop.method, optionalEndpoints.run_stop.path);
|
|
110
|
+
const approval = features['run_approval_response'] === true &&
|
|
111
|
+
features['approval_events'] === true &&
|
|
112
|
+
endpointMatches(endpoints['run_approval'], optionalEndpoints.run_approval.method, optionalEndpoints.run_approval.path);
|
|
113
|
+
return {
|
|
114
|
+
authRequired: auth['required'],
|
|
115
|
+
model,
|
|
116
|
+
features: { approval, cancel },
|
|
117
|
+
fingerprintSource: {
|
|
118
|
+
object: document['object'],
|
|
119
|
+
platform: document['platform'],
|
|
120
|
+
auth: { required: auth['required'], type: auth['type'] },
|
|
121
|
+
features: {
|
|
122
|
+
approval,
|
|
123
|
+
cancel,
|
|
124
|
+
run_events_sse: true,
|
|
125
|
+
run_status: true,
|
|
126
|
+
run_submission: true,
|
|
127
|
+
session_resources: true,
|
|
128
|
+
},
|
|
129
|
+
endpoints: {
|
|
130
|
+
...requiredEndpoints,
|
|
131
|
+
...(approval ? { run_approval: optionalEndpoints.run_approval } : {}),
|
|
132
|
+
...(cancel ? { run_stop: optionalEndpoints.run_stop } : {}),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Produce a non-secret deterministic protocol identity from capabilities. */
|
|
138
|
+
export function compatibilityFingerprint(capabilities) {
|
|
139
|
+
const digest = createHash('sha256')
|
|
140
|
+
.update(stableJson(capabilities.fingerprintSource))
|
|
141
|
+
.digest('hex')
|
|
142
|
+
.slice(0, 16);
|
|
143
|
+
return `capabilities-${digest}`;
|
|
144
|
+
}
|
|
145
|
+
/** Validate and prepare one Hermes Session creation request. */
|
|
146
|
+
export function prepareHermesSession(input) {
|
|
147
|
+
assertEmptyMetadata(input.metadata, 'Session metadata');
|
|
148
|
+
if (input.workspace !== undefined) {
|
|
149
|
+
throw unsupported('session.workspace', 'Hermes API Server workspace selection is not supported.');
|
|
150
|
+
}
|
|
151
|
+
const providerOptions = knownOptions(input.providerOptions, ['source', 'title'], 'Session providerOptions');
|
|
152
|
+
const modelOptions = knownOptions(input.model?.providerOptions, ['modelOptions', 'provider'], 'model providerOptions');
|
|
153
|
+
const body = {};
|
|
154
|
+
const state = {};
|
|
155
|
+
if (input.systemContext !== undefined) {
|
|
156
|
+
const systemContext = inputString(input.systemContext, 'Session system context', true);
|
|
157
|
+
body['system_prompt'] = systemContext;
|
|
158
|
+
state.systemContext = systemContext;
|
|
159
|
+
}
|
|
160
|
+
if (input.model !== undefined) {
|
|
161
|
+
const model = inputSelection(input.model.id, 'model identifier');
|
|
162
|
+
body['model'] = model;
|
|
163
|
+
state.model = model;
|
|
164
|
+
const provider = optionalInputSelection(modelOptions['provider'], 'model Provider identifier');
|
|
165
|
+
if (provider !== undefined) {
|
|
166
|
+
body['provider'] = provider;
|
|
167
|
+
state.provider = provider;
|
|
168
|
+
}
|
|
169
|
+
if (modelOptions['modelOptions'] !== undefined) {
|
|
170
|
+
const nativeOptions = safeOptions(modelOptions['modelOptions'], 'modelOptions');
|
|
171
|
+
body['model_options'] = nativeOptions;
|
|
172
|
+
state.modelOptions = nativeOptions;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const source = optionalInputSelection(providerOptions['source'], 'Session source');
|
|
176
|
+
if (source !== undefined)
|
|
177
|
+
body['source'] = source;
|
|
178
|
+
const title = optionalInputText(providerOptions['title'], 'Session title');
|
|
179
|
+
if (title !== undefined)
|
|
180
|
+
body['title'] = title;
|
|
181
|
+
return { body, state };
|
|
182
|
+
}
|
|
183
|
+
/** Convert portable text and retained Session defaults to a Runs API body. */
|
|
184
|
+
export function prepareHermesRun(input, sessionId, state, options = {}) {
|
|
185
|
+
assertEmptyMetadata(input.metadata, 'Run input metadata');
|
|
186
|
+
assertEmptyMetadata(options.metadata, 'Run metadata');
|
|
187
|
+
const text = [];
|
|
188
|
+
for (const part of input.parts) {
|
|
189
|
+
if (part.type !== 'text') {
|
|
190
|
+
throw unsupported(`input.${part.type}`, 'Hermes API Server portable Runs accept text input only.');
|
|
191
|
+
}
|
|
192
|
+
text.push(inputString(part.text, 'Run text', true));
|
|
193
|
+
}
|
|
194
|
+
if (text.length === 0)
|
|
195
|
+
throw invalidRequest('Hermes Run text is required.');
|
|
196
|
+
const runOptions = knownOptions(options.providerOptions, [], 'Run providerOptions');
|
|
197
|
+
if (Object.keys(runOptions).length > 0) {
|
|
198
|
+
throw invalidRequest('Hermes Run providerOptions are not supported.');
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
input: text.join('\n'),
|
|
202
|
+
session_id: sessionId,
|
|
203
|
+
...(state.systemContext === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { instructions: state.systemContext }),
|
|
206
|
+
...(state.model === undefined ? {} : { model: state.model }),
|
|
207
|
+
...(state.provider === undefined ? {} : { provider: state.provider }),
|
|
208
|
+
...(state.modelOptions === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { model_options: snapshotRecord(state.modelOptions) }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Parse one client-safe Session response. */
|
|
214
|
+
export function parseHermesSession(value) {
|
|
215
|
+
const response = requiredRecord(value, 'Session response');
|
|
216
|
+
if (response['object'] !== 'hermes.session') {
|
|
217
|
+
throw incompatible('Session response');
|
|
218
|
+
}
|
|
219
|
+
const session = requiredRecord(response['session'], 'Session resource');
|
|
220
|
+
const id = nativeIdentifier(session['id'], 'Session identifier');
|
|
221
|
+
const model = optionalSelection(session['model'], 'Session model');
|
|
222
|
+
return { id, ...(model === undefined ? {} : { model }) };
|
|
223
|
+
}
|
|
224
|
+
/** Parse one accepted Run receipt. */
|
|
225
|
+
export function parseHermesRunReceipt(value) {
|
|
226
|
+
const response = requiredRecord(value, 'Run receipt');
|
|
227
|
+
if (response['status'] !== 'started')
|
|
228
|
+
throw incompatible('Run receipt');
|
|
229
|
+
return {
|
|
230
|
+
runId: nativeIdentifier(response['run_id'], 'Run identifier'),
|
|
231
|
+
status: 'started',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Parse and validate one pollable Run status against its owner. */
|
|
235
|
+
export function parseHermesRunStatus(value, expectedRunId, expectedSessionId) {
|
|
236
|
+
const response = requiredRecord(value, 'Run status');
|
|
237
|
+
if (response['object'] !== 'hermes.run' ||
|
|
238
|
+
nativeIdentifier(response['run_id'], 'Run status identifier') !==
|
|
239
|
+
expectedRunId ||
|
|
240
|
+
nativeIdentifier(response['session_id'], 'Run Session identifier') !==
|
|
241
|
+
expectedSessionId) {
|
|
242
|
+
throw incompatible('Run status ownership');
|
|
243
|
+
}
|
|
244
|
+
const status = response['status'];
|
|
245
|
+
if (status === 'queued' ||
|
|
246
|
+
status === 'running' ||
|
|
247
|
+
status === 'waiting_for_approval' ||
|
|
248
|
+
status === 'stopping') {
|
|
249
|
+
return { status };
|
|
250
|
+
}
|
|
251
|
+
if (status === 'completed') {
|
|
252
|
+
assertLastEvent(response, 'run.completed');
|
|
253
|
+
const finalMessage = boundedString(response['output'], 'Run output', true);
|
|
254
|
+
const usage = usageSummary(response['usage']);
|
|
255
|
+
return {
|
|
256
|
+
status,
|
|
257
|
+
terminalEventType: 'run.completed',
|
|
258
|
+
result: {
|
|
259
|
+
status: 'completed',
|
|
260
|
+
finalMessage,
|
|
261
|
+
...(usage === undefined ? {} : { usage }),
|
|
262
|
+
providerResult: { status },
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (status === 'failed') {
|
|
267
|
+
assertLastEvent(response, 'run.failed');
|
|
268
|
+
boundedString(response['error'], 'Run failure', true);
|
|
269
|
+
return {
|
|
270
|
+
status,
|
|
271
|
+
terminalEventType: 'run.failed',
|
|
272
|
+
result: { status: 'failed', providerResult: { status } },
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (status === 'cancelled') {
|
|
276
|
+
assertLastEvent(response, 'run.cancelled');
|
|
277
|
+
return {
|
|
278
|
+
status,
|
|
279
|
+
terminalEventType: 'run.cancelled',
|
|
280
|
+
result: { status: 'cancelled', providerResult: { status } },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
throw incompatible('Run status value');
|
|
284
|
+
}
|
|
285
|
+
/** Parse one Runs API SSE payload into a lifecycle-owned mapping. */
|
|
286
|
+
export function mapHermesEvent(value, expectedRunId) {
|
|
287
|
+
const event = requiredRecord(value, 'Run event');
|
|
288
|
+
const eventType = safeEventType(event['event']);
|
|
289
|
+
if (nativeIdentifier(event['run_id'], 'Run event identifier') !== expectedRunId) {
|
|
290
|
+
throw incompatible('Run event ownership');
|
|
291
|
+
}
|
|
292
|
+
if (event['timestamp'] !== undefined)
|
|
293
|
+
finiteNumber(event['timestamp'], 'timestamp');
|
|
294
|
+
if (eventType === 'message.delta') {
|
|
295
|
+
return {
|
|
296
|
+
kind: 'portable',
|
|
297
|
+
event: {
|
|
298
|
+
type: 'message.delta',
|
|
299
|
+
data: { delta: boundedString(event['delta'], 'message delta', true) },
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (eventType === 'tool.started') {
|
|
304
|
+
return {
|
|
305
|
+
kind: 'portable',
|
|
306
|
+
event: {
|
|
307
|
+
type: 'tool.started',
|
|
308
|
+
data: { tool: boundedString(event['tool'], 'tool name') },
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (eventType === 'tool.completed') {
|
|
313
|
+
const error = event['error'];
|
|
314
|
+
if (typeof error !== 'boolean')
|
|
315
|
+
throw incompatible('tool completion');
|
|
316
|
+
return {
|
|
317
|
+
kind: 'portable',
|
|
318
|
+
event: {
|
|
319
|
+
type: 'tool.completed',
|
|
320
|
+
data: {
|
|
321
|
+
tool: boundedString(event['tool'], 'tool name'),
|
|
322
|
+
duration: finiteNumber(event['duration'], 'tool duration'),
|
|
323
|
+
error,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (eventType === 'reasoning.available') {
|
|
329
|
+
return {
|
|
330
|
+
kind: 'portable',
|
|
331
|
+
event: {
|
|
332
|
+
type: 'reasoning.completed',
|
|
333
|
+
data: { text: boundedString(event['text'], 'reasoning text', true) },
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (eventType === 'run.completed' ||
|
|
338
|
+
eventType === 'run.failed' ||
|
|
339
|
+
eventType === 'run.cancelled') {
|
|
340
|
+
return { kind: 'terminal', eventType };
|
|
341
|
+
}
|
|
342
|
+
if (eventType === 'approval.request') {
|
|
343
|
+
const choices = stringArray(event['choices'], 'approval choices');
|
|
344
|
+
if (choices.length === 0 ||
|
|
345
|
+
choices.some((choice) => !approvalChoices.has(choice))) {
|
|
346
|
+
throw incompatible('approval choices');
|
|
347
|
+
}
|
|
348
|
+
const title = optionalText(event['description'], 'approval description');
|
|
349
|
+
const prompt = optionalText(event['command'], 'approval command');
|
|
350
|
+
return {
|
|
351
|
+
kind: 'interaction',
|
|
352
|
+
choices,
|
|
353
|
+
request: {
|
|
354
|
+
kind: 'approval',
|
|
355
|
+
...(title === undefined ? {} : { title }),
|
|
356
|
+
...(prompt === undefined ? {} : { prompt }),
|
|
357
|
+
providerState: { choices },
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (eventType === 'approval.responded') {
|
|
362
|
+
const choice = boundedSelection(event['choice'], 'approval choice');
|
|
363
|
+
const resolved = nonnegativeInteger(event['resolved'], 'approval resolved count');
|
|
364
|
+
if (!approvalChoices.has(choice) || resolved < 1) {
|
|
365
|
+
throw incompatible('approval resolution');
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
kind: 'interaction.resolved',
|
|
369
|
+
choice,
|
|
370
|
+
resolved,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (eventType === 'subagent.start' || eventType === 'subagent.complete') {
|
|
374
|
+
const childSessionId = optionalNativeIdentifier(event['child_session_id']);
|
|
375
|
+
if (childSessionId !== undefined) {
|
|
376
|
+
const subagentId = optionalNativeIdentifier(event['subagent_id']);
|
|
377
|
+
const status = optionalSelection(event['status'], 'subagent status');
|
|
378
|
+
return {
|
|
379
|
+
kind: 'subagent',
|
|
380
|
+
event: {
|
|
381
|
+
eventType,
|
|
382
|
+
runId: expectedRunId,
|
|
383
|
+
childSessionId,
|
|
384
|
+
...(subagentId === undefined ? {} : { subagentId }),
|
|
385
|
+
...(status === undefined ? {} : { status }),
|
|
386
|
+
raw: redactHermesEvent(event),
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
kind: 'provider',
|
|
393
|
+
event: {
|
|
394
|
+
providerEventType: eventType,
|
|
395
|
+
raw: redactHermesEvent(event),
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** Parse a stopping acknowledgement without treating it as a terminal. */
|
|
400
|
+
export function parseHermesStopResponse(value, expectedRunId) {
|
|
401
|
+
const response = requiredRecord(value, 'Run stop response');
|
|
402
|
+
if (nativeIdentifier(response['run_id'], 'Run stop identifier') !==
|
|
403
|
+
expectedRunId ||
|
|
404
|
+
response['status'] !== 'stopping') {
|
|
405
|
+
throw incompatible('Run stop response');
|
|
406
|
+
}
|
|
407
|
+
return { status: 'stopping' };
|
|
408
|
+
}
|
|
409
|
+
/** Parse one acknowledgement for a Run-scoped approval response. */
|
|
410
|
+
export function parseHermesApprovalResponse(value, expectedRunId, expectedChoice) {
|
|
411
|
+
const response = requiredRecord(value, 'approval response');
|
|
412
|
+
const choice = boundedSelection(response['choice'], 'approval choice');
|
|
413
|
+
const resolved = nonnegativeInteger(response['resolved'], 'approval resolved count');
|
|
414
|
+
if (response['object'] !== 'hermes.run.approval_response' ||
|
|
415
|
+
nativeIdentifier(response['run_id'], 'approval Run identifier') !==
|
|
416
|
+
expectedRunId ||
|
|
417
|
+
choice !== expectedChoice ||
|
|
418
|
+
resolved < 1) {
|
|
419
|
+
throw incompatible('approval response');
|
|
420
|
+
}
|
|
421
|
+
return { choice, resolved };
|
|
422
|
+
}
|
|
423
|
+
/** Restore only the bounded non-secret Session defaults owned by this Adapter. */
|
|
424
|
+
export function sessionStateFromRef(ref) {
|
|
425
|
+
const value = requiredRecordOrMismatch(ref.providerState);
|
|
426
|
+
try {
|
|
427
|
+
const model = optionalSelection(value['model'], 'persisted model');
|
|
428
|
+
const provider = optionalSelection(value['provider'], 'persisted Provider');
|
|
429
|
+
const systemContext = optionalText(value['systemContext'], 'persisted system context');
|
|
430
|
+
const known = new Set(['model', 'provider', 'systemContext']);
|
|
431
|
+
if (Object.keys(value).some((key) => !known.has(key))) {
|
|
432
|
+
throw sessionStateMismatch();
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
...(model === undefined ? {} : { model }),
|
|
436
|
+
...(provider === undefined ? {} : { provider }),
|
|
437
|
+
...(systemContext === undefined ? {} : { systemContext }),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
throw sessionStateMismatch();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Snapshot Session defaults without retaining caller-owned objects. */
|
|
445
|
+
export function snapshotHermesSessionState(state) {
|
|
446
|
+
return {
|
|
447
|
+
...(state.model === undefined ? {} : { model: state.model }),
|
|
448
|
+
...(state.provider === undefined ? {} : { provider: state.provider }),
|
|
449
|
+
...(state.systemContext === undefined
|
|
450
|
+
? {}
|
|
451
|
+
: { systemContext: state.systemContext }),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Produce a bounded content-free representation for raw observation. */
|
|
455
|
+
export function redactHermesEvent(value) {
|
|
456
|
+
return redact(value, 0, { nodes: 0 });
|
|
457
|
+
}
|
|
458
|
+
function endpointMatches(value, method, path) {
|
|
459
|
+
const endpoint = record(value);
|
|
460
|
+
return endpoint?.['method'] === method && endpoint['path'] === path;
|
|
461
|
+
}
|
|
462
|
+
function assertEndpoint(value, method, path, name) {
|
|
463
|
+
if (!endpointMatches(value, method, path)) {
|
|
464
|
+
throw incompatible(`endpoint ${name}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function assertLastEvent(response, expected) {
|
|
468
|
+
if (response['last_event'] !== expected) {
|
|
469
|
+
throw incompatible('Run terminal evidence');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function usageSummary(value) {
|
|
473
|
+
if (value === undefined)
|
|
474
|
+
return undefined;
|
|
475
|
+
const usage = requiredRecord(value, 'Run usage');
|
|
476
|
+
const inputTokens = optionalNonnegativeInteger(usage['input_tokens'], 'input_tokens');
|
|
477
|
+
const outputTokens = optionalNonnegativeInteger(usage['output_tokens'], 'output_tokens');
|
|
478
|
+
const totalTokens = optionalNonnegativeInteger(usage['total_tokens'], 'total_tokens');
|
|
479
|
+
return {
|
|
480
|
+
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
481
|
+
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
482
|
+
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function knownOptions(value, allowed, label) {
|
|
486
|
+
if (value === undefined)
|
|
487
|
+
return {};
|
|
488
|
+
const options = record(value);
|
|
489
|
+
if (options === undefined)
|
|
490
|
+
throw invalidRequest(`Hermes ${label} must be an object.`);
|
|
491
|
+
const known = new Set(allowed);
|
|
492
|
+
const unknown = Object.keys(options).find((name) => !known.has(name));
|
|
493
|
+
if (unknown !== undefined) {
|
|
494
|
+
throw invalidRequest(`Hermes ${label} field ${unknown} is unknown.`);
|
|
495
|
+
}
|
|
496
|
+
return options;
|
|
497
|
+
}
|
|
498
|
+
function assertEmptyMetadata(value, label) {
|
|
499
|
+
if (value !== undefined &&
|
|
500
|
+
(record(value) === undefined || Object.keys(value).length > 0)) {
|
|
501
|
+
throw invalidRequest(`Hermes ${label} are not supported.`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function safeOptions(value, label) {
|
|
505
|
+
const options = record(value);
|
|
506
|
+
if (options === undefined)
|
|
507
|
+
throw invalidRequest(`Hermes ${label} must be an object.`);
|
|
508
|
+
const state = { nodes: 0 };
|
|
509
|
+
const copied = safeOptionValue(options, 0, state, '');
|
|
510
|
+
return copied;
|
|
511
|
+
}
|
|
512
|
+
function safeOptionValue(value, depth, state, key) {
|
|
513
|
+
state.nodes += 1;
|
|
514
|
+
if (state.nodes > maximumOptionNodes || depth > maximumOptionDepth) {
|
|
515
|
+
throw invalidRequest('Hermes modelOptions exceed the supported bound.');
|
|
516
|
+
}
|
|
517
|
+
if (credentialShapedOptionKey(key)) {
|
|
518
|
+
throw invalidRequest('Hermes modelOptions cannot contain credential fields.');
|
|
519
|
+
}
|
|
520
|
+
if (value === null || typeof value === 'boolean')
|
|
521
|
+
return value;
|
|
522
|
+
if (typeof value === 'number') {
|
|
523
|
+
if (!Number.isFinite(value))
|
|
524
|
+
throw invalidRequest('Hermes modelOptions contain a non-finite number.');
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
if (typeof value === 'string')
|
|
528
|
+
return inputString(value, 'model option', true);
|
|
529
|
+
if (Array.isArray(value)) {
|
|
530
|
+
if (value.length > maximumRawArrayItems) {
|
|
531
|
+
throw invalidRequest('Hermes modelOptions array exceeds the supported bound.');
|
|
532
|
+
}
|
|
533
|
+
return value.map((item) => safeOptionValue(item, depth + 1, state, key));
|
|
534
|
+
}
|
|
535
|
+
const object = record(value);
|
|
536
|
+
if (object === undefined ||
|
|
537
|
+
Object.keys(object).length > maximumRawObjectFields) {
|
|
538
|
+
throw invalidRequest('Hermes modelOptions contain an unsupported value.');
|
|
539
|
+
}
|
|
540
|
+
return Object.fromEntries(Object.entries(object).map(([name, child]) => [
|
|
541
|
+
name,
|
|
542
|
+
safeOptionValue(child, depth + 1, state, name),
|
|
543
|
+
]));
|
|
544
|
+
}
|
|
545
|
+
function credentialShapedOptionKey(key) {
|
|
546
|
+
if (sensitiveOptionSuffix.test(key) || sensitiveCompactKeySuffix.test(key)) {
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
const words = key
|
|
550
|
+
.replace(/([\p{Ll}\d])(\p{Lu})/gu, '$1 $2')
|
|
551
|
+
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1 $2')
|
|
552
|
+
.split(/[^\p{L}\p{N}]+/u);
|
|
553
|
+
return words.some((word) => sensitiveOptionWords.has(word.toLowerCase()));
|
|
554
|
+
}
|
|
555
|
+
function stringArray(value, label) {
|
|
556
|
+
if (!Array.isArray(value) || value.length > maximumRawArrayItems) {
|
|
557
|
+
throw incompatible(label);
|
|
558
|
+
}
|
|
559
|
+
return value.map((item) => boundedSelection(item, label));
|
|
560
|
+
}
|
|
561
|
+
function boundedSelection(value, label) {
|
|
562
|
+
const selected = boundedString(value, label);
|
|
563
|
+
if (/\p{Cc}/u.test(selected))
|
|
564
|
+
throw incompatible(label);
|
|
565
|
+
return selected;
|
|
566
|
+
}
|
|
567
|
+
function optionalSelection(value, label) {
|
|
568
|
+
return value === undefined || value === null
|
|
569
|
+
? undefined
|
|
570
|
+
: boundedSelection(value, label);
|
|
571
|
+
}
|
|
572
|
+
function optionalText(value, label) {
|
|
573
|
+
return value === undefined || value === null
|
|
574
|
+
? undefined
|
|
575
|
+
: boundedString(value, label, true);
|
|
576
|
+
}
|
|
577
|
+
function inputSelection(value, label) {
|
|
578
|
+
const selected = inputString(value, label);
|
|
579
|
+
if (/\p{Cc}/u.test(selected)) {
|
|
580
|
+
throw invalidRequest(`Hermes ${label} contains control characters.`);
|
|
581
|
+
}
|
|
582
|
+
return selected;
|
|
583
|
+
}
|
|
584
|
+
function optionalInputSelection(value, label) {
|
|
585
|
+
return value === undefined || value === null
|
|
586
|
+
? undefined
|
|
587
|
+
: inputSelection(value, label);
|
|
588
|
+
}
|
|
589
|
+
function optionalInputText(value, label) {
|
|
590
|
+
return value === undefined || value === null
|
|
591
|
+
? undefined
|
|
592
|
+
: inputString(value, label, true);
|
|
593
|
+
}
|
|
594
|
+
function inputString(value, label, allowEmpty = false) {
|
|
595
|
+
if (typeof value !== 'string' ||
|
|
596
|
+
(!allowEmpty && value.length === 0) ||
|
|
597
|
+
value.length > maximumTextLength) {
|
|
598
|
+
throw invalidRequest(`Hermes ${label} is invalid.`);
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function boundedString(value, label, allowEmpty = false) {
|
|
603
|
+
if (typeof value !== 'string' ||
|
|
604
|
+
(!allowEmpty && value.length === 0) ||
|
|
605
|
+
value.length > maximumTextLength) {
|
|
606
|
+
throw incompatible(label);
|
|
607
|
+
}
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
function nativeIdentifier(value, label) {
|
|
611
|
+
const id = boundedString(value, label);
|
|
612
|
+
if (id.length > maximumIdentifierLength ||
|
|
613
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(id)) {
|
|
614
|
+
throw incompatible(label);
|
|
615
|
+
}
|
|
616
|
+
return id;
|
|
617
|
+
}
|
|
618
|
+
function optionalNativeIdentifier(value) {
|
|
619
|
+
if (value === undefined || value === null)
|
|
620
|
+
return undefined;
|
|
621
|
+
try {
|
|
622
|
+
return nativeIdentifier(value, 'native identifier');
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function finiteNumber(value, label) {
|
|
629
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
630
|
+
throw incompatible(label);
|
|
631
|
+
}
|
|
632
|
+
return value;
|
|
633
|
+
}
|
|
634
|
+
function nonnegativeInteger(value, label) {
|
|
635
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
636
|
+
throw incompatible(label);
|
|
637
|
+
}
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
function optionalNonnegativeInteger(value, label) {
|
|
641
|
+
return value === undefined ? undefined : nonnegativeInteger(value, label);
|
|
642
|
+
}
|
|
643
|
+
function requiredRecord(value, label) {
|
|
644
|
+
const object = record(value);
|
|
645
|
+
if (object === undefined)
|
|
646
|
+
throw incompatible(label);
|
|
647
|
+
return object;
|
|
648
|
+
}
|
|
649
|
+
function requiredRecordOrMismatch(value) {
|
|
650
|
+
const object = record(value);
|
|
651
|
+
if (object === undefined)
|
|
652
|
+
throw sessionStateMismatch();
|
|
653
|
+
return object;
|
|
654
|
+
}
|
|
655
|
+
function record(value) {
|
|
656
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
657
|
+
? value
|
|
658
|
+
: undefined;
|
|
659
|
+
}
|
|
660
|
+
function safeEventType(value) {
|
|
661
|
+
return typeof value === 'string' &&
|
|
662
|
+
/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(value)
|
|
663
|
+
? value
|
|
664
|
+
: 'unknown';
|
|
665
|
+
}
|
|
666
|
+
function redact(value, depth, state) {
|
|
667
|
+
state.nodes += 1;
|
|
668
|
+
if (state.nodes > maximumRawNodes || depth > maximumRawDepth) {
|
|
669
|
+
return '<truncated>';
|
|
670
|
+
}
|
|
671
|
+
if (value === null)
|
|
672
|
+
return null;
|
|
673
|
+
if (typeof value === 'string')
|
|
674
|
+
return '<string>';
|
|
675
|
+
if (typeof value === 'number') {
|
|
676
|
+
return Number.isFinite(value) ? '<number>' : '<non-finite>';
|
|
677
|
+
}
|
|
678
|
+
if (typeof value === 'boolean')
|
|
679
|
+
return value;
|
|
680
|
+
if (Array.isArray(value)) {
|
|
681
|
+
return value
|
|
682
|
+
.slice(0, maximumRawArrayItems)
|
|
683
|
+
.map((item) => redact(item, depth + 1, state));
|
|
684
|
+
}
|
|
685
|
+
const object = record(value);
|
|
686
|
+
if (object === undefined)
|
|
687
|
+
return `<${typeof value}>`;
|
|
688
|
+
const output = {};
|
|
689
|
+
let included = 0;
|
|
690
|
+
let omitted = 0;
|
|
691
|
+
for (const [key, child] of Object.entries(object)) {
|
|
692
|
+
if (!safeRawKeys.has(key) || included >= maximumRawObjectFields) {
|
|
693
|
+
omitted += 1;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
output[key] = redact(child, depth + 1, state);
|
|
697
|
+
included += 1;
|
|
698
|
+
}
|
|
699
|
+
if (omitted > 0)
|
|
700
|
+
output['omittedFields'] = omitted;
|
|
701
|
+
return output;
|
|
702
|
+
}
|
|
703
|
+
function snapshotRecord(value) {
|
|
704
|
+
return structuredClone(value);
|
|
705
|
+
}
|
|
706
|
+
function stableJson(value) {
|
|
707
|
+
if (value === null || typeof value !== 'object')
|
|
708
|
+
return JSON.stringify(value);
|
|
709
|
+
if (Array.isArray(value))
|
|
710
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
711
|
+
const object = value;
|
|
712
|
+
return `{${Object.keys(object)
|
|
713
|
+
.sort()
|
|
714
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`)
|
|
715
|
+
.join(',')}}`;
|
|
716
|
+
}
|
|
717
|
+
function incompatible(surface) {
|
|
718
|
+
return new HarnessError('provider_api_incompatible', `Hermes Agent returned an incompatible ${surface}.`, { retryable: false, providerId: HERMES_PROVIDER_ID });
|
|
719
|
+
}
|
|
720
|
+
function invalidRequest(message) {
|
|
721
|
+
return new HarnessError('invalid_request', message, {
|
|
722
|
+
retryable: false,
|
|
723
|
+
providerId: HERMES_PROVIDER_ID,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function unsupported(capability, message) {
|
|
727
|
+
return new HarnessError('unsupported_capability', message, {
|
|
728
|
+
retryable: false,
|
|
729
|
+
providerId: HERMES_PROVIDER_ID,
|
|
730
|
+
details: { capability },
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function sessionStateMismatch() {
|
|
734
|
+
return new HarnessError('session_provider_mismatch', 'Hermes Agent Session state is missing or incompatible.', { retryable: false, providerId: HERMES_PROVIDER_ID });
|
|
735
|
+
}
|
|
736
|
+
//# sourceMappingURL=protocol.js.map
|