@rudderhq/agent-runtime-hermes-gateway 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ export declare const type = "hermes_gateway";
2
+ export declare const label = "Hermes API Server";
3
+ export declare const models: {
4
+ id: string;
5
+ label: string;
6
+ }[];
7
+ export declare const agentConfigurationDoc = "# hermes_gateway agent configuration\n\nAdapter: hermes_gateway\n\nUse when Rudder should invoke a running Hermes API Server over HTTP.\n\nCore fields:\n- url (string, required): Hermes API Server base URL (http:// or https://)\n- apiKey/authToken/token (string, optional): API_SERVER_KEY bearer token\n- model (string, optional): Hermes model or configured model route alias\n- timeoutSec (number, optional): total run budget (default 120)\n- sessionKeyStrategy (issue|fixed|run, optional): upstream Hermes session mapping\n- sessionKey (string, optional): fixed session key when strategy=fixed\n- payloadTemplate (object, optional): extra /v1/runs fields\n\nHermes tool and approval events are projected into Rudder as bounded\nsynthetic_tool_continuity evidence. They are never labeled native or lossless.\n";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,mBAAmB,CAAC;AACrC,eAAO,MAAM,KAAK,sBAAsB,CAAC;AACzC,eAAO,MAAM,MAAM,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAO,CAAC;AAE1D,eAAO,MAAM,qBAAqB,+yBAiBjC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ export const type = "hermes_gateway";
2
+ export const label = "Hermes API Server";
3
+ export const models = [];
4
+ export const agentConfigurationDoc = `# hermes_gateway agent configuration
5
+
6
+ Adapter: hermes_gateway
7
+
8
+ Use when Rudder should invoke a running Hermes API Server over HTTP.
9
+
10
+ Core fields:
11
+ - url (string, required): Hermes API Server base URL (http:// or https://)
12
+ - apiKey/authToken/token (string, optional): API_SERVER_KEY bearer token
13
+ - model (string, optional): Hermes model or configured model route alias
14
+ - timeoutSec (number, optional): total run budget (default 120)
15
+ - sessionKeyStrategy (issue|fixed|run, optional): upstream Hermes session mapping
16
+ - sessionKey (string, optional): fixed session key when strategy=fixed
17
+ - payloadTemplate (object, optional): extra /v1/runs fields
18
+
19
+ Hermes tool and approval events are projected into Rudder as bounded
20
+ synthetic_tool_continuity evidence. They are never labeled native or lossless.
21
+ `;
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,CAAC;AACrC,MAAM,CAAC,MAAM,KAAK,GAAG,mBAAmB,CAAC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAoC,EAAE,CAAC;AAE1D,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;CAiBpC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { AgentRuntimeExecutionContext, AgentRuntimeExecutionResult } from "@rudderhq/agent-runtime-utils";
2
+ export declare function execute(ctx: AgentRuntimeExecutionContext): Promise<AgentRuntimeExecutionResult>;
3
+ //# sourceMappingURL=execute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/server/execute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AA2R/G,wBAAsB,OAAO,CAAC,GAAG,EAAE,4BAA4B,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAiWrG"}
@@ -0,0 +1,610 @@
1
+ import { asNumber, asString, parseObject } from "@rudderhq/agent-runtime-utils/server-utils";
2
+ import { createHash } from "node:crypto";
3
+ import { asRecord, baseUrl, endpoint, hasBearerAuth, positiveMs, preflightBaseUrl, requestHeaders, requestJson, textFrom } from "./http.js";
4
+ const MAX_PROJECTED_EVENTS = 200;
5
+ const MAX_PROJECTED_EVENT_BYTES = 64 * 1024;
6
+ const MAX_PROJECTED_CONTEXT_BYTES = 512 * 1024;
7
+ const MAX_PROJECTED_TOKENS = 32_000;
8
+ const MAX_SAFE_EVENT_TEXT = MAX_PROJECTED_EVENT_BYTES;
9
+ const STOP_RECONCILIATION_MS = 1_500;
10
+ function sessionKey(ctx) {
11
+ const config = parseObject(ctx.config);
12
+ const configured = asString(config.sessionKey, "").trim();
13
+ const strategy = asString(config.sessionKeyStrategy, "issue").trim().toLowerCase();
14
+ const issueId = asString(ctx.context.issueId ?? ctx.context.taskId, "").trim();
15
+ if (strategy === "run")
16
+ return `rudder:run:${ctx.runId}`;
17
+ if (strategy === "fixed" && configured)
18
+ return configured;
19
+ if (issueId)
20
+ return `rudder:issue:${issueId}`;
21
+ return configured || `rudder:run:${ctx.runId}`;
22
+ }
23
+ function storedSessionId(ctx) {
24
+ const runtimeParams = parseObject(ctx.runtime.sessionParams);
25
+ const configured = asString(runtimeParams.hermesSessionId ?? runtimeParams.sessionId, "").trim();
26
+ return configured || asString(ctx.runtime.sessionId, "").trim() || null;
27
+ }
28
+ function responseSessionId(body) {
29
+ const session = asRecord(body.session);
30
+ const id = asString(session?.id ?? body.id ?? body.session_id, "").trim();
31
+ return id || null;
32
+ }
33
+ async function resolveHermesSession(params) {
34
+ let providerSessionId = storedSessionId(params.ctx);
35
+ let created = false;
36
+ if (providerSessionId) {
37
+ const current = await requestJson(endpoint(params.base, `/api/sessions/${encodeURIComponent(providerSessionId)}`), params.config, {}, params.requestTimeout);
38
+ if (!current.response.ok) {
39
+ throw new Error(`Hermes session mapping could not be read (HTTP ${current.response.status}).`);
40
+ }
41
+ const returnedId = responseSessionId(current.body);
42
+ if (!returnedId || returnedId !== providerSessionId) {
43
+ throw new Error("Hermes session mapping returned a conflicting provider ID.");
44
+ }
45
+ }
46
+ else {
47
+ const createdResponse = await requestJson(endpoint(params.base, "/api/sessions"), params.config, {
48
+ method: "POST",
49
+ headers: { "content-type": "application/json" },
50
+ body: JSON.stringify({
51
+ source: "rudder",
52
+ ...(params.model ? { model: params.model } : {}),
53
+ }),
54
+ }, params.requestTimeout);
55
+ if (!createdResponse.response.ok) {
56
+ throw new Error(`Hermes session mapping could not be created (HTTP ${createdResponse.response.status}).`);
57
+ }
58
+ providerSessionId = responseSessionId(createdResponse.body);
59
+ if (!providerSessionId) {
60
+ throw new Error("Hermes session creation did not return a provider session ID.");
61
+ }
62
+ created = true;
63
+ }
64
+ const messages = await requestJson(endpoint(params.base, `/api/sessions/${encodeURIComponent(providerSessionId)}/messages`), params.config, {}, params.requestTimeout);
65
+ if (!messages.response.ok) {
66
+ throw new Error(`Hermes session messages could not be read (HTTP ${messages.response.status}).`);
67
+ }
68
+ const messageData = Array.isArray(messages.body.data) ? messages.body.data : [];
69
+ const returnedMessageSessionId = asString(messages.body.session_id, "").trim();
70
+ if (returnedMessageSessionId && returnedMessageSessionId !== providerSessionId) {
71
+ throw new Error("Hermes session messages returned a conflicting provider ID.");
72
+ }
73
+ return { providerSessionId, messageCount: messageData.length, created };
74
+ }
75
+ function shortHash(value) {
76
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
77
+ }
78
+ function boundedText(value) {
79
+ const text = textFrom(value);
80
+ return text ? text.slice(0, MAX_SAFE_EVENT_TEXT) : null;
81
+ }
82
+ function buildToolContextProjection(ctx, sessionId) {
83
+ const raw = Array.isArray(ctx.context.rudderToolContext)
84
+ ? ctx.context.rudderToolContext
85
+ : Array.isArray(ctx.context.transcript)
86
+ ? ctx.context.transcript
87
+ : [];
88
+ const source = raw.slice(0, MAX_PROJECTED_EVENTS).map((entry) => asRecord(entry) ?? {});
89
+ const ids = new Set();
90
+ const projected = source.map((entry, index) => {
91
+ const kind = asString(entry.kind ?? entry.type ?? entry.event, "event").trim().slice(0, 64);
92
+ const toolCallId = asString(entry.toolCallId ?? entry.tool_call_id ?? entry.id, "").trim().slice(0, 160) || null;
93
+ if (kind === "tool_call" && toolCallId)
94
+ ids.add(toolCallId);
95
+ const content = textFrom(entry.content ?? entry.text ?? entry.output) ?? "";
96
+ return {
97
+ index,
98
+ kind,
99
+ tool: asString(entry.tool ?? entry.name, "").trim().slice(0, 160) || null,
100
+ toolCallId,
101
+ approvalId: asString(entry.approvalId ?? entry.approval_id, "").trim().slice(0, 160) || null,
102
+ status: asString(entry.status, "").trim().slice(0, 64) || null,
103
+ contentHash: shortHash(boundedText(content) ?? ""),
104
+ contentBytes: Buffer.byteLength(content, "utf8"),
105
+ };
106
+ });
107
+ const unpaired = projected.some((entry) => (entry.kind === "tool_result" || entry.kind === "approval") && entry.toolCallId && !ids.has(entry.toolCallId));
108
+ const projection = {
109
+ version: "RUDDER_TOOL_CONTEXT_V1",
110
+ sessionId,
111
+ transcriptHash: shortHash(projected),
112
+ events: projected,
113
+ };
114
+ const serialized = JSON.stringify(projection);
115
+ const projectedBytes = Buffer.byteLength(serialized, "utf8");
116
+ const refusal = raw.length > MAX_PROJECTED_EVENTS
117
+ ? "canonical tool context exceeds the 200-event limit"
118
+ : projected.some((entry) => entry.contentBytes > MAX_PROJECTED_EVENT_BYTES)
119
+ ? "a canonical event exceeds the 64 KiB per-event limit"
120
+ : projectedBytes > MAX_PROJECTED_CONTEXT_BYTES
121
+ ? "bounded tool context exceeds the 512 KiB aggregate limit"
122
+ : Math.ceil(projectedBytes / 4) > MAX_PROJECTED_TOKENS
123
+ ? "bounded tool context exceeds the 32,000-token estimate"
124
+ : unpaired
125
+ ? "causal tool/approval pairing is incomplete"
126
+ : null;
127
+ const bounded = refusal ? JSON.stringify({ version: "RUDDER_TOOL_CONTEXT_V1", sessionId, transcriptHash: shortHash([]), events: [] }) : serialized;
128
+ return {
129
+ text: `\n\nRUDDER_TOOL_CONTEXT_V1\n${bounded}`,
130
+ hash: shortHash(bounded),
131
+ eventCount: refusal ? 0 : projected.length,
132
+ refusal,
133
+ };
134
+ }
135
+ function runMessage(ctx, toolContext) {
136
+ const context = ctx.context;
137
+ const chatPrompt = context.chatMode === true ? asString(context.chatPrompt, "").trim() : "";
138
+ if (chatPrompt)
139
+ return `${chatPrompt}${toolContext}`;
140
+ const reason = asString(context.wakeReason, "manual");
141
+ const issueId = asString(context.issueId ?? context.taskId, "");
142
+ return [
143
+ "Rudder wake event for Hermes API Server.",
144
+ `run_id=${ctx.runId}`,
145
+ `agent_id=${ctx.agent.id}`,
146
+ `wake_reason=${reason}`,
147
+ issueId ? `issue_id=${issueId}` : null,
148
+ "Use the authenticated Rudder context for this run and return a concise result.",
149
+ ].filter(Boolean).join("\n") + toolContext;
150
+ }
151
+ function terminalStatus(status) {
152
+ return ["completed", "failed", "cancelled", "stopped", "error"].includes(status.toLowerCase());
153
+ }
154
+ function statusFromTerminalEvent(event) {
155
+ const kind = asString(event?.event, "");
156
+ if (kind === "run.completed")
157
+ return "completed";
158
+ if (kind === "run.failed")
159
+ return "failed";
160
+ if (kind === "run.cancelled")
161
+ return "cancelled";
162
+ return null;
163
+ }
164
+ function eventText(event) {
165
+ return textFrom(event.delta) ?? textFrom(event.output) ?? textFrom(event.content) ?? textFrom(event.message);
166
+ }
167
+ function usageFrom(value) {
168
+ const record = asRecord(value);
169
+ if (!record)
170
+ return undefined;
171
+ const inputTokens = asNumber(record.input_tokens ?? record.inputTokens, 0);
172
+ const outputTokens = asNumber(record.output_tokens ?? record.outputTokens, 0);
173
+ return inputTokens || outputTokens ? { inputTokens, outputTokens } : undefined;
174
+ }
175
+ function safeEvent(event) {
176
+ return {
177
+ event: asString(event.event, "event").slice(0, 80),
178
+ run_id: asString(event.run_id, "").slice(0, 120) || undefined,
179
+ tool: asString(event.tool ?? event.name, "").slice(0, 160) || undefined,
180
+ tool_call_id: asString(event.tool_call_id ?? event.call_id, "").slice(0, 160) || undefined,
181
+ approval_id: asString(event.approval_id ?? event.approvalId, "").slice(0, 160) || undefined,
182
+ status: asString(event.status, "").slice(0, 64) || undefined,
183
+ choice: asString(event.choice, "").slice(0, 32) || undefined,
184
+ deltaHash: event.delta !== undefined ? shortHash(boundedText(event.delta) ?? "") : undefined,
185
+ outputHash: event.output !== undefined ? shortHash(boundedText(event.output) ?? "") : undefined,
186
+ };
187
+ }
188
+ function boundedLogText(value) {
189
+ return typeof value === "string" ? value.slice(0, MAX_SAFE_EVENT_TEXT) : undefined;
190
+ }
191
+ function transcriptLogEvent(event) {
192
+ return {
193
+ ...safeEvent(event),
194
+ // Keep transcript text available while bounding each field independently.
195
+ // The result projection above remains hash-only.
196
+ delta: boundedLogText(event.delta),
197
+ output: boundedLogText(event.output),
198
+ content: boundedLogText(event.content),
199
+ message: boundedLogText(event.message),
200
+ error: boundedLogText(event.error),
201
+ };
202
+ }
203
+ async function consumeSse(response, onEvent) {
204
+ if (!response.body)
205
+ return { malformed: true };
206
+ const reader = response.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ let malformed = false;
210
+ while (true) {
211
+ const { done, value } = await reader.read();
212
+ if (done)
213
+ break;
214
+ buffer += decoder.decode(value, { stream: true });
215
+ const lines = buffer.split(/\r?\n/);
216
+ buffer = lines.pop() ?? "";
217
+ for (const line of lines) {
218
+ if (!line.startsWith("data:"))
219
+ continue;
220
+ const raw = line.slice(5).trim();
221
+ if (!raw)
222
+ continue;
223
+ try {
224
+ const parsed = JSON.parse(raw);
225
+ const event = asRecord(parsed);
226
+ if (event)
227
+ await onEvent(event);
228
+ }
229
+ catch {
230
+ // Ignore malformed keepalive/application fragments; terminal status is
231
+ // reconciled with GET /v1/runs below.
232
+ malformed = true;
233
+ }
234
+ }
235
+ }
236
+ if (buffer.trim())
237
+ malformed = true;
238
+ return { malformed };
239
+ }
240
+ export async function execute(ctx) {
241
+ const config = parseObject(ctx.config);
242
+ const base = baseUrl(config.url);
243
+ if (!base)
244
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: "Hermes API Server URL is missing or invalid.", errorCode: "hermes_gateway_url_invalid" };
245
+ const endpointPreflight = await preflightBaseUrl(base);
246
+ if (!endpointPreflight.ok)
247
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: endpointPreflight.reason, errorCode: "hermes_gateway_endpoint_rejected" };
248
+ if (!hasBearerAuth(config))
249
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: "Hermes API Server requires an explicit Bearer API key.", errorCode: "hermes_gateway_bearer_missing" };
250
+ const timeoutMs = positiveMs(config.timeoutMs ?? (asNumber(config.timeoutSec, 120) * 1000), 120_000);
251
+ const requestTimeout = Math.min(timeoutMs, 15_000);
252
+ const template = parseObject(config.payloadTemplate);
253
+ const workstreamKey = sessionKey(ctx);
254
+ const preliminaryToolContext = buildToolContextProjection(ctx, workstreamKey);
255
+ if (preliminaryToolContext.refusal) {
256
+ return {
257
+ exitCode: 1,
258
+ signal: null,
259
+ timedOut: false,
260
+ errorMessage: `Hermes tool context refused: ${preliminaryToolContext.refusal}.`,
261
+ errorCode: "hermes_gateway_continuity_refused",
262
+ resultJson: {
263
+ synthetic_tool_continuity: {
264
+ mode: "synthetic_tool_continuity",
265
+ native: false,
266
+ lossless: false,
267
+ projectionVersion: "RUDDER_TOOL_CONTEXT_V1",
268
+ refusal: preliminaryToolContext.refusal,
269
+ },
270
+ },
271
+ };
272
+ }
273
+ let session;
274
+ try {
275
+ session = await resolveHermesSession({
276
+ base,
277
+ config,
278
+ ctx,
279
+ model: asString(config.model, "").trim(),
280
+ requestTimeout,
281
+ });
282
+ }
283
+ catch (error) {
284
+ return {
285
+ exitCode: 1,
286
+ signal: null,
287
+ timedOut: false,
288
+ errorMessage: error instanceof Error ? error.message : "Hermes session mapping failed.",
289
+ errorCode: "hermes_gateway_session_mapping_failed",
290
+ resultJson: { sessionMapping: { mode: "hermes_sessions_api_v1", verified: false } },
291
+ };
292
+ }
293
+ const toolContext = buildToolContextProjection(ctx, session.providerSessionId);
294
+ if (toolContext.refusal) {
295
+ return {
296
+ exitCode: 1,
297
+ signal: null,
298
+ timedOut: false,
299
+ errorMessage: `Hermes tool context refused: ${toolContext.refusal}.`,
300
+ errorCode: "hermes_gateway_continuity_refused",
301
+ sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId },
302
+ sessionDisplayId: session.providerSessionId,
303
+ resultJson: {
304
+ synthetic_tool_continuity: {
305
+ mode: "synthetic_tool_continuity",
306
+ native: false,
307
+ lossless: false,
308
+ projectionVersion: "RUDDER_TOOL_CONTEXT_V1",
309
+ refusal: toolContext.refusal,
310
+ },
311
+ },
312
+ };
313
+ }
314
+ const input = runMessage(ctx, toolContext.text);
315
+ const body = {
316
+ ...template,
317
+ input,
318
+ session_id: session.providerSessionId,
319
+ idempotency_key: ctx.runId,
320
+ ...(asString(config.model, "").trim() ? { model: asString(config.model, "").trim() } : {}),
321
+ };
322
+ delete body.message;
323
+ if (ctx.onMeta) {
324
+ await ctx.onMeta({ agentRuntimeType: "hermes_gateway", command: "hermes-api", commandArgs: ["POST", endpoint(base, "/v1/runs").toString()], context: ctx.context });
325
+ }
326
+ await ctx.onLog("stdout", `[hermes-gateway] submitting run upstream=hermes-api base=${base.origin}\n`);
327
+ let upstreamRunId = null;
328
+ let stopSent = false;
329
+ let stopStartedAt = null;
330
+ let stopRequestAccepted = false;
331
+ let stopRequestError = null;
332
+ const stopUpstream = async () => {
333
+ if (!upstreamRunId)
334
+ return false;
335
+ if (stopSent)
336
+ return stopRequestAccepted;
337
+ stopSent = true;
338
+ stopStartedAt = Date.now();
339
+ try {
340
+ const response = await requestJson(endpoint(base, `/v1/runs/${encodeURIComponent(upstreamRunId)}/stop`), config, { method: "POST", body: JSON.stringify({ reason: "rudder_abort" }), headers: { "content-type": "application/json" } }, requestTimeout);
341
+ stopRequestAccepted = response.response.ok && ["stopping", "cancelled", "stopped"].includes(asString(response.body.status, "").toLowerCase());
342
+ if (!stopRequestAccepted)
343
+ stopRequestError = `HTTP ${response.response.status}`;
344
+ await ctx.onLog("stdout", `[hermes-gateway] stop requested upstreamRunId=${upstreamRunId} accepted=${stopRequestAccepted}\n`);
345
+ }
346
+ catch (error) {
347
+ stopRequestError = error instanceof Error ? error.message : String(error);
348
+ await ctx.onLog("stderr", `[hermes-gateway] stop request failed upstreamRunId=${upstreamRunId}\n`);
349
+ }
350
+ return stopRequestAccepted;
351
+ };
352
+ const abortHandler = () => { void stopUpstream(); };
353
+ ctx.abortSignal?.addEventListener("abort", abortHandler, { once: true });
354
+ const started = await requestJson(endpoint(base, "/v1/runs"), config, { method: "POST", headers: { "content-type": "application/json", "x-hermes-session-key": workstreamKey }, body: JSON.stringify(body) }, requestTimeout);
355
+ if (!started.response.ok) {
356
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
357
+ const message = textFrom(started.body) ?? `Hermes run submission returned HTTP ${started.response.status}`;
358
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: message, errorCode: "hermes_gateway_submission_failed", resultJson: started.body };
359
+ }
360
+ upstreamRunId = asString(started.body.run_id ?? started.body.id, "").trim() || null;
361
+ if (!upstreamRunId) {
362
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
363
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: "Hermes API Server did not return a run_id.", errorCode: "hermes_gateway_submission_indeterminate", resultJson: started.body };
364
+ }
365
+ await ctx.onLog("stdout", `[hermes-gateway] run accepted upstreamRunId=${upstreamRunId}\n`);
366
+ let controlLease = null;
367
+ if (ctx.controlAttempt) {
368
+ controlLease = await ctx.controlAttempt.register({
369
+ runtimeType: "hermes_gateway",
370
+ providerThreadId: session.providerSessionId,
371
+ providerTurnId: upstreamRunId,
372
+ capabilities: { steer: "interrupt_continue", interrupt: "remote" },
373
+ async steer() {
374
+ return { disposition: "unsupported", reason: "Hermes Runs does not expose native steer." };
375
+ },
376
+ async interrupt() {
377
+ return (await stopUpstream()) ? "acknowledged" : "unverified";
378
+ },
379
+ async dispose() { },
380
+ });
381
+ if (!controlLease) {
382
+ await stopUpstream();
383
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
384
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: "Hermes runtime control lease was lost.", errorCode: "hermes_gateway_control_lost", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId };
385
+ }
386
+ }
387
+ const startedAt = Date.now();
388
+ const events = [];
389
+ const assistant = [];
390
+ let latestStatus = { ...started.body };
391
+ let terminalEvent = null;
392
+ let sseConnected = false;
393
+ let sseMalformed = false;
394
+ let sseError = null;
395
+ let approvalPending = false;
396
+ let approvalResolved = false;
397
+ let approvalError = null;
398
+ let approvalDecision = null;
399
+ let rudderApprovalId = null;
400
+ const approvalChoice = (value) => {
401
+ const normalized = asString(value, "").trim().toLowerCase();
402
+ if (["approved", "approve", "allow", "once"].includes(normalized))
403
+ return "once";
404
+ if (["deny", "denied", "rejected", "reject"].includes(normalized))
405
+ return "deny";
406
+ return null;
407
+ };
408
+ const resolveApproval = async (requestedChoice) => {
409
+ const choice = requestedChoice ?? approvalChoice(ctx.context.approvalStatus);
410
+ if (!choice)
411
+ return;
412
+ const response = await requestJson(endpoint(base, `/v1/runs/${encodeURIComponent(upstreamRunId)}/approval`), config, { method: "POST", body: JSON.stringify({ choice }), headers: { "content-type": "application/json" } }, requestTimeout);
413
+ if (!response.response.ok)
414
+ throw new Error(`Hermes approval returned HTTP ${response.response.status}`);
415
+ approvalResolved = true;
416
+ approvalDecision = choice;
417
+ await ctx.onLog("stdout", `[hermes-gateway] approval resolved upstreamRunId=${upstreamRunId} choice=${choice}\n`);
418
+ };
419
+ const recordEvent = async (event) => {
420
+ if (events.length < MAX_PROJECTED_EVENTS)
421
+ events.push(event);
422
+ const kind = asString(event.event, "event");
423
+ await ctx.onLog("stdout", `[hermes-gateway:event] run=${upstreamRunId} type=${kind} data=${JSON.stringify(transcriptLogEvent(event))}\n`);
424
+ // Preserve leading/trailing whitespace in streaming deltas. `textFrom`
425
+ // intentionally trims ordinary result fields, but trimming each delta
426
+ // corrupts word boundaries when Hermes splits a sentence across events.
427
+ const text = kind === "message.delta" && typeof event.delta === "string"
428
+ ? event.delta
429
+ : eventText(event);
430
+ if (kind === "message.delta" && text)
431
+ assistant.push(text);
432
+ if (["run.completed", "run.failed", "run.cancelled"].includes(kind))
433
+ terminalEvent = event;
434
+ if (kind === "approval.request") {
435
+ approvalPending = true;
436
+ try {
437
+ const configuredChoice = approvalChoice(ctx.context.approvalStatus);
438
+ if (configuredChoice) {
439
+ await resolveApproval(configuredChoice);
440
+ }
441
+ else if (ctx.requestApproval && ctx.waitForApproval) {
442
+ const request = await ctx.requestApproval({
443
+ type: "agent_runtime",
444
+ payload: {
445
+ provider: "hermes",
446
+ runtimeType: "hermes_gateway",
447
+ upstreamRunId,
448
+ sessionId: session.providerSessionId,
449
+ event: safeEvent(event),
450
+ choices: ["once", "deny"],
451
+ },
452
+ });
453
+ rudderApprovalId = request.id;
454
+ await ctx.onLog("stdout", `[hermes-gateway] Rudder approval requested id=${request.id} upstreamRunId=${upstreamRunId}\n`);
455
+ const decision = await ctx.waitForApproval(request.id, timeoutMs);
456
+ if (decision.status === "approved")
457
+ await resolveApproval("once");
458
+ else if (decision.status === "rejected")
459
+ await resolveApproval("deny");
460
+ else
461
+ approvalError = "Hermes approval remained unresolved before the bounded wait expired.";
462
+ }
463
+ else {
464
+ approvalError = "Hermes approval is pending and no Rudder approval bridge is available.";
465
+ }
466
+ }
467
+ catch (error) {
468
+ approvalError = error instanceof Error ? error.message : String(error);
469
+ }
470
+ await ctx.onLog("stderr", `[hermes-gateway] approval required upstreamRunId=${upstreamRunId} resolved=${approvalResolved}\n`);
471
+ }
472
+ };
473
+ try {
474
+ const sseController = new AbortController();
475
+ const abortSse = () => sseController.abort();
476
+ ctx.abortSignal?.addEventListener("abort", abortSse, { once: true });
477
+ const sseTimer = setTimeout(() => sseController.abort(), timeoutMs);
478
+ try {
479
+ const sse = await fetch(endpoint(base, `/v1/runs/${encodeURIComponent(upstreamRunId)}/events`), { headers: requestHeaders(config), redirect: "error", signal: sseController.signal });
480
+ if (sse.ok) {
481
+ sseConnected = true;
482
+ const stream = await consumeSse(sse, recordEvent);
483
+ sseMalformed = stream.malformed;
484
+ }
485
+ else {
486
+ sseError = `events_http_${sse.status}`;
487
+ await ctx.onLog("stderr", `[hermes-gateway] events endpoint returned HTTP ${sse.status}; reconciling status\n`);
488
+ }
489
+ }
490
+ finally {
491
+ clearTimeout(sseTimer);
492
+ ctx.abortSignal?.removeEventListener("abort", abortSse);
493
+ }
494
+ }
495
+ catch (error) {
496
+ sseError = error instanceof Error ? error.message : "events_stream_failed";
497
+ await ctx.onLog("stderr", `[hermes-gateway] events stream ended: ${error instanceof Error ? error.message : String(error)}\n`);
498
+ }
499
+ // Hermes emits the terminal lifecycle event before closing the SSE stream,
500
+ // while the initial POST response intentionally remains `started`. Preserve
501
+ // that authoritative terminal observation so a completed run cannot fall
502
+ // through to the bounded timeout loop when the status GET races the stream.
503
+ const observedTerminalEvent = terminalEvent;
504
+ const eventStatus = statusFromTerminalEvent(observedTerminalEvent);
505
+ if (eventStatus && !terminalStatus(asString(latestStatus.status, ""))) {
506
+ latestStatus = {
507
+ ...latestStatus,
508
+ status: eventStatus,
509
+ ...(observedTerminalEvent?.output !== undefined ? { output: observedTerminalEvent.output } : {}),
510
+ ...(observedTerminalEvent?.error !== undefined ? { error: observedTerminalEvent.error } : {}),
511
+ ...(observedTerminalEvent?.usage !== undefined ? { usage: observedTerminalEvent.usage } : {}),
512
+ };
513
+ }
514
+ const reconciliationDeadline = () => stopSent && stopStartedAt ? stopStartedAt + STOP_RECONCILIATION_MS : startedAt + timeoutMs;
515
+ while (!terminalStatus(asString(latestStatus.status, "")) && Date.now() < reconciliationDeadline()) {
516
+ if (ctx.abortSignal?.aborted) {
517
+ await stopUpstream();
518
+ }
519
+ await new Promise((resolve) => setTimeout(resolve, 250));
520
+ try {
521
+ const polled = await requestJson(endpoint(base, `/v1/runs/${encodeURIComponent(upstreamRunId)}`), config, {}, Math.min(requestTimeout, 5_000));
522
+ latestStatus = polled.body;
523
+ const status = asString(latestStatus.status, "");
524
+ if (terminalStatus(status))
525
+ break;
526
+ }
527
+ catch {
528
+ // Keep the last authenticated status and allow the bounded timeout to
529
+ // produce an honest indeterminate result.
530
+ }
531
+ }
532
+ ctx.abortSignal?.removeEventListener("abort", abortHandler);
533
+ await controlLease?.release();
534
+ if (!terminalStatus(asString(latestStatus.status, "")) && !stopSent) {
535
+ await stopUpstream();
536
+ while (!terminalStatus(asString(latestStatus.status, "")) && Date.now() < reconciliationDeadline()) {
537
+ await new Promise((resolve) => setTimeout(resolve, 250));
538
+ try {
539
+ const polled = await requestJson(endpoint(base, `/v1/runs/${encodeURIComponent(upstreamRunId)}`), config, {}, Math.min(requestTimeout, 5_000));
540
+ latestStatus = polled.body;
541
+ }
542
+ catch {
543
+ // Keep the last authenticated status; the final result records that cancellation was unverified.
544
+ }
545
+ }
546
+ }
547
+ const status = asString(latestStatus.status, "").toLowerCase();
548
+ const terminal = terminalEvent;
549
+ const output = assistant.join("").trim() || textFrom(terminal) || textFrom(latestStatus.output) || null;
550
+ const usage = usageFrom(terminal?.usage ?? latestStatus.usage);
551
+ const continuity = {
552
+ mode: "synthetic_tool_continuity",
553
+ native: false,
554
+ lossless: false,
555
+ projectionVersion: "RUDDER_TOOL_CONTEXT_V1",
556
+ maxEvents: MAX_PROJECTED_EVENTS,
557
+ maxEventBytes: MAX_PROJECTED_EVENT_BYTES,
558
+ maxAggregateBytes: MAX_PROJECTED_CONTEXT_BYTES,
559
+ maxTokenEstimate: MAX_PROJECTED_TOKENS,
560
+ eventCount: events.length,
561
+ toolContextHash: toolContext.hash,
562
+ projectedEventCount: toolContext.eventCount,
563
+ };
564
+ if (approvalPending && !approvalResolved && !approvalError) {
565
+ approvalError = "Hermes approval is pending and no Rudder approval decision was supplied.";
566
+ }
567
+ const resultJson = {
568
+ upstreamRunId,
569
+ status,
570
+ output,
571
+ events: events.map((event) => safeEvent(event)),
572
+ synthetic_tool_continuity: continuity,
573
+ sessionMapping: {
574
+ mode: "hermes_sessions_api_v1",
575
+ providerSessionId: session.providerSessionId,
576
+ created: session.created,
577
+ verified: true,
578
+ messageCount: session.messageCount,
579
+ },
580
+ control: { stopRequested: stopSent, stopAccepted: stopRequestAccepted, stopRequestError },
581
+ approval: { pending: approvalPending, resolved: approvalResolved, decision: approvalDecision, rudderApprovalId, error: approvalError },
582
+ eventCompleteness: {
583
+ status: terminalEvent
584
+ ? (sseMalformed ? "partial" : "complete")
585
+ : (sseConnected || sseError ? "partial" : "terminal_only"),
586
+ sseConnected,
587
+ terminalEventObserved: Boolean(terminalEvent),
588
+ malformedEvents: sseMalformed,
589
+ eventCount: events.length,
590
+ reason: sseError ?? (sseMalformed ? "malformed_sse_event" : terminalEvent ? null : "terminal_status_reconciled_without_terminal_event"),
591
+ },
592
+ };
593
+ if (approvalError) {
594
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: approvalError, errorCode: "hermes_gateway_approval_unresolved", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson };
595
+ }
596
+ if (ctx.abortSignal?.aborted || stopSent || status === "cancelled" || status === "stopped") {
597
+ if (!terminalStatus(status) || !["cancelled", "stopped"].includes(status)) {
598
+ return { exitCode: 1, signal: "SIGTERM", timedOut: false, errorMessage: "Hermes stop was requested but terminal state was not verified.", errorCode: "hermes_gateway_cancel_unverified", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson, ...(output ? { summary: output } : {}) };
599
+ }
600
+ return { exitCode: 1, signal: "SIGTERM", timedOut: false, errorMessage: "Hermes run stopped.", errorCode: "hermes_gateway_stopped", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson, ...(output ? { summary: output } : {}) };
601
+ }
602
+ if (!terminalStatus(status) || Date.now() - startedAt >= timeoutMs) {
603
+ return { exitCode: 1, signal: null, timedOut: !stopSent, errorMessage: stopSent ? "Hermes stop was requested but terminal state was not verified." : `Hermes run timed out after ${timeoutMs}ms.`, errorCode: stopSent ? "hermes_gateway_cancel_unverified" : "hermes_gateway_timeout", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson };
604
+ }
605
+ if (status !== "completed") {
606
+ return { exitCode: 1, signal: null, timedOut: false, errorMessage: textFrom(latestStatus.error) ?? `Hermes run ended with status ${status}.`, errorCode: "hermes_gateway_run_failed", sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson, ...(output ? { summary: output } : {}) };
607
+ }
608
+ return { exitCode: 0, signal: null, timedOut: false, provider: "hermes", model: asString(latestStatus.model, "") || null, ...(usage ? { usage } : {}), sessionParams: { sessionId: session.providerSessionId, hermesSessionId: session.providerSessionId }, sessionDisplayId: session.providerSessionId, resultJson, ...(output ? { summary: output } : {}) };
609
+ }
610
+ //# sourceMappingURL=execute.js.map