@mongodb-js/agent-engine-runner-shared 0.11.3

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.
Files changed (220) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +29 -0
  4. package/dist/agent_config.d.ts +167 -0
  5. package/dist/agent_config.d.ts.map +1 -0
  6. package/dist/agent_config.js +544 -0
  7. package/dist/call_interrupted.d.ts +12 -0
  8. package/dist/call_interrupted.d.ts.map +1 -0
  9. package/dist/call_interrupted.js +11 -0
  10. package/dist/checkpoint_workspace.d.ts +25 -0
  11. package/dist/checkpoint_workspace.d.ts.map +1 -0
  12. package/dist/checkpoint_workspace.js +44 -0
  13. package/dist/context.d.ts +235 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +322 -0
  16. package/dist/db_config.d.ts +28 -0
  17. package/dist/db_config.d.ts.map +1 -0
  18. package/dist/db_config.js +66 -0
  19. package/dist/db_naming.d.ts +54 -0
  20. package/dist/db_naming.d.ts.map +1 -0
  21. package/dist/db_naming.js +94 -0
  22. package/dist/error_reporting.d.ts +67 -0
  23. package/dist/error_reporting.d.ts.map +1 -0
  24. package/dist/error_reporting.js +311 -0
  25. package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
  26. package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
  27. package/dist/generated/workflow/v1/activity_pb.js +115 -0
  28. package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
  29. package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
  30. package/dist/generated/workflow/v1/common_pb.js +86 -0
  31. package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
  32. package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
  33. package/dist/generated/workflow/v1/runtime_pb.js +40 -0
  34. package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
  35. package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
  36. package/dist/generated/workflow/v1/state_pb.js +68 -0
  37. package/dist/guardrails_evaluator/core.d.ts +23 -0
  38. package/dist/guardrails_evaluator/core.d.ts.map +1 -0
  39. package/dist/guardrails_evaluator/core.js +122 -0
  40. package/dist/guardrails_evaluator/index.d.ts +10 -0
  41. package/dist/guardrails_evaluator/index.d.ts.map +1 -0
  42. package/dist/guardrails_evaluator/index.js +11 -0
  43. package/dist/guardrails_evaluator/regex.d.ts +20 -0
  44. package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
  45. package/dist/guardrails_evaluator/regex.js +233 -0
  46. package/dist/hooks.d.ts +109 -0
  47. package/dist/hooks.d.ts.map +1 -0
  48. package/dist/hooks.js +216 -0
  49. package/dist/http_path.d.ts +18 -0
  50. package/dist/http_path.d.ts.map +1 -0
  51. package/dist/http_path.js +53 -0
  52. package/dist/index.d.ts +35 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +41 -0
  55. package/dist/launcher.d.ts +130 -0
  56. package/dist/launcher.d.ts.map +1 -0
  57. package/dist/launcher.js +325 -0
  58. package/dist/logger.d.ts +96 -0
  59. package/dist/logger.d.ts.map +1 -0
  60. package/dist/logger.js +204 -0
  61. package/dist/mcp_oauth.d.ts +51 -0
  62. package/dist/mcp_oauth.d.ts.map +1 -0
  63. package/dist/mcp_oauth.js +389 -0
  64. package/dist/mcp_oauth_secret.d.ts +21 -0
  65. package/dist/mcp_oauth_secret.d.ts.map +1 -0
  66. package/dist/mcp_oauth_secret.js +122 -0
  67. package/dist/mcp_tools.d.ts +71 -0
  68. package/dist/mcp_tools.d.ts.map +1 -0
  69. package/dist/mcp_tools.js +301 -0
  70. package/dist/memory_appbound.d.ts +42 -0
  71. package/dist/memory_appbound.d.ts.map +1 -0
  72. package/dist/memory_appbound.js +159 -0
  73. package/dist/memory_writer.d.ts +49 -0
  74. package/dist/memory_writer.d.ts.map +1 -0
  75. package/dist/memory_writer.js +171 -0
  76. package/dist/metrics.d.ts +84 -0
  77. package/dist/metrics.d.ts.map +1 -0
  78. package/dist/metrics.js +205 -0
  79. package/dist/models.d.ts +1458 -0
  80. package/dist/models.d.ts.map +1 -0
  81. package/dist/models.js +1726 -0
  82. package/dist/node_logger.d.ts +43 -0
  83. package/dist/node_logger.d.ts.map +1 -0
  84. package/dist/node_logger.js +158 -0
  85. package/dist/owner_callback.d.ts +16 -0
  86. package/dist/owner_callback.d.ts.map +1 -0
  87. package/dist/owner_callback.js +40 -0
  88. package/dist/progress.d.ts +57 -0
  89. package/dist/progress.d.ts.map +1 -0
  90. package/dist/progress.js +140 -0
  91. package/dist/runtime.d.ts +131 -0
  92. package/dist/runtime.d.ts.map +1 -0
  93. package/dist/runtime.js +351 -0
  94. package/dist/secure_llm_proxy.d.ts +115 -0
  95. package/dist/secure_llm_proxy.d.ts.map +1 -0
  96. package/dist/secure_llm_proxy.js +922 -0
  97. package/dist/secure_wrapper.d.ts +332 -0
  98. package/dist/secure_wrapper.d.ts.map +1 -0
  99. package/dist/secure_wrapper.js +1249 -0
  100. package/dist/server/aer.d.ts +61 -0
  101. package/dist/server/aer.d.ts.map +1 -0
  102. package/dist/server/aer.js +1124 -0
  103. package/dist/server/auth.d.ts +56 -0
  104. package/dist/server/auth.d.ts.map +1 -0
  105. package/dist/server/auth.js +132 -0
  106. package/dist/server/base.d.ts +104 -0
  107. package/dist/server/base.d.ts.map +1 -0
  108. package/dist/server/base.js +150 -0
  109. package/dist/server/callInterrupt.d.ts +49 -0
  110. package/dist/server/callInterrupt.d.ts.map +1 -0
  111. package/dist/server/callInterrupt.js +68 -0
  112. package/dist/server/callback_delivery.d.ts +14 -0
  113. package/dist/server/callback_delivery.d.ts.map +1 -0
  114. package/dist/server/callback_delivery.js +141 -0
  115. package/dist/server/chunk_types.d.ts +50 -0
  116. package/dist/server/chunk_types.d.ts.map +1 -0
  117. package/dist/server/chunk_types.js +62 -0
  118. package/dist/server/cors.d.ts +52 -0
  119. package/dist/server/cors.d.ts.map +1 -0
  120. package/dist/server/cors.js +107 -0
  121. package/dist/server/drain.d.ts +169 -0
  122. package/dist/server/drain.d.ts.map +1 -0
  123. package/dist/server/drain.js +455 -0
  124. package/dist/server/function.d.ts +77 -0
  125. package/dist/server/function.d.ts.map +1 -0
  126. package/dist/server/function.js +337 -0
  127. package/dist/server/http_retry.d.ts +37 -0
  128. package/dist/server/http_retry.d.ts.map +1 -0
  129. package/dist/server/http_retry.js +157 -0
  130. package/dist/server/index.d.ts +7 -0
  131. package/dist/server/index.d.ts.map +1 -0
  132. package/dist/server/index.js +5 -0
  133. package/dist/server/metadata.d.ts +50 -0
  134. package/dist/server/metadata.d.ts.map +1 -0
  135. package/dist/server/metadata.js +193 -0
  136. package/dist/server/oe_url.d.ts +36 -0
  137. package/dist/server/oe_url.d.ts.map +1 -0
  138. package/dist/server/oe_url.js +50 -0
  139. package/dist/server/owner_url.d.ts +35 -0
  140. package/dist/server/owner_url.d.ts.map +1 -0
  141. package/dist/server/owner_url.js +146 -0
  142. package/dist/server/query.d.ts +42 -0
  143. package/dist/server/query.d.ts.map +1 -0
  144. package/dist/server/query.js +28 -0
  145. package/dist/server/tool.d.ts +138 -0
  146. package/dist/server/tool.d.ts.map +1 -0
  147. package/dist/server/tool.js +1017 -0
  148. package/dist/span_names.d.ts +21 -0
  149. package/dist/span_names.d.ts.map +1 -0
  150. package/dist/span_names.js +31 -0
  151. package/dist/structured_logging/constants.d.ts +17 -0
  152. package/dist/structured_logging/constants.d.ts.map +1 -0
  153. package/dist/structured_logging/constants.js +71 -0
  154. package/dist/structured_logging/env.d.ts +18 -0
  155. package/dist/structured_logging/env.d.ts.map +1 -0
  156. package/dist/structured_logging/env.js +39 -0
  157. package/dist/structured_logging/install.d.ts +56 -0
  158. package/dist/structured_logging/install.d.ts.map +1 -0
  159. package/dist/structured_logging/install.js +107 -0
  160. package/dist/structured_logging/layout.d.ts +9 -0
  161. package/dist/structured_logging/layout.d.ts.map +1 -0
  162. package/dist/structured_logging/layout.js +144 -0
  163. package/dist/structured_logging/serialize.d.ts +27 -0
  164. package/dist/structured_logging/serialize.d.ts.map +1 -0
  165. package/dist/structured_logging/serialize.js +61 -0
  166. package/dist/structured_logging/stdio_capture.d.ts +59 -0
  167. package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
  168. package/dist/structured_logging/stdio_capture.js +164 -0
  169. package/dist/structured_logging/uncaught.d.ts +14 -0
  170. package/dist/structured_logging/uncaught.d.ts.map +1 -0
  171. package/dist/structured_logging/uncaught.js +58 -0
  172. package/dist/structured_logging.d.ts +48 -0
  173. package/dist/structured_logging.d.ts.map +1 -0
  174. package/dist/structured_logging.js +47 -0
  175. package/dist/tls_client.d.ts +61 -0
  176. package/dist/tls_client.d.ts.map +1 -0
  177. package/dist/tls_client.js +298 -0
  178. package/dist/tool_api_error.d.ts +62 -0
  179. package/dist/tool_api_error.d.ts.map +1 -0
  180. package/dist/tool_api_error.js +399 -0
  181. package/dist/tool_memory_ownership.d.ts +10 -0
  182. package/dist/tool_memory_ownership.d.ts.map +1 -0
  183. package/dist/tool_memory_ownership.js +36 -0
  184. package/dist/toolpod_handlers.d.ts +126 -0
  185. package/dist/toolpod_handlers.d.ts.map +1 -0
  186. package/dist/toolpod_handlers.js +1016 -0
  187. package/dist/tracing/exporters.d.ts +51 -0
  188. package/dist/tracing/exporters.d.ts.map +1 -0
  189. package/dist/tracing/exporters.js +327 -0
  190. package/dist/tracing/index.d.ts +3 -0
  191. package/dist/tracing/index.d.ts.map +1 -0
  192. package/dist/tracing/index.js +2 -0
  193. package/dist/tracing/setup.d.ts +76 -0
  194. package/dist/tracing/setup.d.ts.map +1 -0
  195. package/dist/tracing/setup.js +436 -0
  196. package/dist/utils.d.ts +204 -0
  197. package/dist/utils.d.ts.map +1 -0
  198. package/dist/utils.js +867 -0
  199. package/dist/workflow/activity.d.ts +71 -0
  200. package/dist/workflow/activity.d.ts.map +1 -0
  201. package/dist/workflow/activity.js +357 -0
  202. package/dist/workflow/attempt.d.ts +12 -0
  203. package/dist/workflow/attempt.d.ts.map +1 -0
  204. package/dist/workflow/attempt.js +96 -0
  205. package/dist/workflow/client.d.ts +46 -0
  206. package/dist/workflow/client.d.ts.map +1 -0
  207. package/dist/workflow/client.js +299 -0
  208. package/dist/workflow/context.d.ts +37 -0
  209. package/dist/workflow/context.d.ts.map +1 -0
  210. package/dist/workflow/context.js +350 -0
  211. package/dist/workflow/heartbeat.d.ts +15 -0
  212. package/dist/workflow/heartbeat.d.ts.map +1 -0
  213. package/dist/workflow/heartbeat.js +78 -0
  214. package/dist/workflow/index.d.ts +14 -0
  215. package/dist/workflow/index.d.ts.map +1 -0
  216. package/dist/workflow/index.js +10 -0
  217. package/dist/workflow/memory.d.ts +17 -0
  218. package/dist/workflow/memory.d.ts.map +1 -0
  219. package/dist/workflow/memory.js +184 -0
  220. package/package.json +73 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Function mode for the Runner SDK.
3
+ *
4
+ * Function mode runs a tool CLI-style: the process boots, runs a single named
5
+ * tool from the registry, reports the result to the OE, and exits. It is the
6
+ * per-call counterpart to the long-running tool server (mode `tool`).
7
+ *
8
+ * Mirrors Python's `agent_engine_runner_shared/server/function.py`.
9
+ *
10
+ * How the invocation arrives: fctr seeds `RunRequest.metadata` into the guest
11
+ * metadata directory before the workload starts. The OE sets one key,
12
+ * `request`, to a JSON `ToolFunctionRequest` envelope carrying both the
13
+ * `ToolPodExecuteRequest` and the tool-call `step` number (required by
14
+ * `ToolResultRequest`, owned by OE dispatch). The result is POSTed to
15
+ * `{oe_url}/tool/result`, the same endpoint the AER already uses.
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import os from "node:os";
20
+ import { z } from "zod";
21
+ import { getLogger } from "../logger.js";
22
+ import { ownerDelivered } from "../owner_callback.js";
23
+ import { getCurrentExecutionMetadata, runWithCustomerOrigin, runWithExecutionContext, } from "../context.js";
24
+ import { ToolPodExecuteRequestSchema, } from "../models.js";
25
+ import { populateLlmRegistryFromEntrypoint } from "../hooks.js";
26
+ import { getRequestTimeout, logToolRequest, toolRedactFields, } from "../utils.js";
27
+ import { fetchPlatform, getFetchOptionsWithTLS } from "../tls_client.js";
28
+ import { executeErrorResponse, requestCredentialValues, } from "../tool_api_error.js";
29
+ import { getCurrentTraceContext } from "../tracing/setup.js";
30
+ import { discardResponseBody, retryAfterWaitMs } from "./http_retry.js";
31
+ import { resolveOeUrl } from "./oe_url.js";
32
+ import { resolveOwnerUrl } from "./owner_url.js";
33
+ const logger = getLogger("agent_engine_runner_shared.server.function");
34
+ // Guest metadata directory, mirroring fctr's util.MetadataDir. fctr
35
+ // materializes RunRequest.metadata into <dir>/<key> files before the
36
+ // workload starts. A fixed convention: the platform does not announce it.
37
+ const METADATA_DIR = "/run/meta";
38
+ const REQUEST_KEY = "request";
39
+ const TOOL_RESULT_PATH = "/tool/result";
40
+ const MAX_POST_ATTEMPTS = 3;
41
+ const POST_BASE_DELAY_MS = 100;
42
+ /** The function-mode invocation envelope delivered at /run/meta/request. */
43
+ export const ToolFunctionRequestSchema = z.object({
44
+ request: ToolPodExecuteRequestSchema,
45
+ step: z.number().int(),
46
+ });
47
+ function readMetadata(metadataDir, key) {
48
+ const filePath = path.join(metadataDir, key);
49
+ let value;
50
+ try {
51
+ value = fs.readFileSync(filePath, "utf-8");
52
+ }
53
+ catch (e) {
54
+ if (e.code === "ENOENT") {
55
+ throw new Error(`function mode requires metadata key '${key}' at ${filePath}`, { cause: e });
56
+ }
57
+ throw e;
58
+ }
59
+ if (!value.trim()) {
60
+ throw new Error(`function mode metadata key '${key}' at ${filePath} is empty`);
61
+ }
62
+ return value;
63
+ }
64
+ function readFunctionRequest(metadataDir) {
65
+ const raw = readMetadata(metadataDir, REQUEST_KEY);
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(raw);
69
+ }
70
+ catch (e) {
71
+ throw new Error(`metadata key '${REQUEST_KEY}' is not a valid ToolFunctionRequest: ${e.message}`, { cause: e });
72
+ }
73
+ const result = ToolFunctionRequestSchema.safeParse(parsed);
74
+ if (!result.success) {
75
+ throw new Error(`metadata key '${REQUEST_KEY}' is not a valid ToolFunctionRequest: ${result.error.message}`);
76
+ }
77
+ return result.data;
78
+ }
79
+ function buildResultRequest(request, response, step, durationMs) {
80
+ const { traceId, spanId } = getCurrentTraceContext();
81
+ return {
82
+ execution_id: request.execution_id,
83
+ step_number: step,
84
+ tool_name: request.tool_name,
85
+ status: response.status,
86
+ result: response.result,
87
+ error: response.error,
88
+ duration_ms: durationMs,
89
+ pod_name: response.pod_name,
90
+ kind: response.kind,
91
+ metadata: response.metadata ?? {},
92
+ trace_id: traceId ?? undefined,
93
+ span_id: spanId ?? undefined,
94
+ tool_api_error: response.tool_api_error,
95
+ };
96
+ }
97
+ /**
98
+ * Encode the result to a JSON-safe object for the /tool/result POST.
99
+ *
100
+ * A tool may return a value the JSON encoder cannot handle (e.g. a circular
101
+ * reference). Downgrade any encode failure to an error report so the OE still
102
+ * receives a terminal signal. Mirrors Python's _result_payload.
103
+ */
104
+ export function buildResultPayload(result, executionId) {
105
+ try {
106
+ JSON.stringify(result);
107
+ return result;
108
+ }
109
+ catch (e) {
110
+ logger.error(`function mode: result for execution_id=${executionId} is not JSON-serializable: ${e.message}`);
111
+ return {
112
+ execution_id: result.execution_id,
113
+ step_number: result.step_number,
114
+ tool_name: result.tool_name,
115
+ status: "error",
116
+ result: undefined,
117
+ duration_ms: result.duration_ms,
118
+ pod_name: result.pod_name,
119
+ kind: result.kind,
120
+ metadata: {},
121
+ error: `tool result is not JSON-serializable: ${e.message}`,
122
+ };
123
+ }
124
+ }
125
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
126
+ /**
127
+ * POST body to url with bounded exponential backoff.
128
+ *
129
+ * Retries on transient transport errors and 5xx responses. Does not retry on
130
+ * 4xx. Re-throws the final error on exhaustion. Mirrors Python's
131
+ * post_json_with_retries and the shared postWithRetries server helper.
132
+ *
133
+ * `ownerUrl` — when given (already validated against `url` and path-appended by
134
+ * the caller), a single owner pre-attempt runs *before* the service loop and
135
+ * does not consume the service retry budget (so the trusted `url` always gets
136
+ * its full `maxAttempts`; total tries are `maxAttempts + 1`). The owner is
137
+ * best-effort: any failure — a transport error OR any non-2xx response (4xx or
138
+ * 5xx) — marks the replica unusable, is logged, and falls through to the
139
+ * service loop. The owner is never retried and an owner response never throws.
140
+ * When the trusted `url` returns 503 with a delta-seconds `Retry-After`, that
141
+ * attempt waits the capped hint instead of the exponential backoff.
142
+ */
143
+ async function postWithRetries(url, body, ownerUrl, onOwnerFailure, maxAttempts = MAX_POST_ATTEMPTS, baseDelayMs = POST_BASE_DELAY_MS) {
144
+ const timeoutMs = getRequestTimeout() * 1000;
145
+ const serialized = JSON.stringify(body);
146
+ // Owner pre-attempt per the shared owner-callback policy (owner_callback.ts):
147
+ // a single best-effort POST that does not consume the service retry budget.
148
+ if (ownerUrl != null) {
149
+ if (await ownerDelivered(ownerUrl, () => ({
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: serialized,
153
+ signal: AbortSignal.timeout(timeoutMs),
154
+ ...getFetchOptionsWithTLS(ownerUrl),
155
+ }), (msg) => logger.warn(`function mode: /tool/result ${msg} URL ${url}`))) {
156
+ return;
157
+ }
158
+ onOwnerFailure?.();
159
+ }
160
+ const tlsOptions = getFetchOptionsWithTLS(url);
161
+ let lastError = null;
162
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
163
+ let retryWaitMs = null;
164
+ let response;
165
+ try {
166
+ response = await fetchPlatform(url, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: serialized,
170
+ signal: AbortSignal.timeout(timeoutMs),
171
+ ...tlsOptions,
172
+ });
173
+ }
174
+ catch (e) {
175
+ lastError = e;
176
+ }
177
+ if (response) {
178
+ const ok = response.ok;
179
+ const status = response.status;
180
+ const retryAfter = response.headers?.get?.("Retry-After") ?? null;
181
+ await discardResponseBody(response);
182
+ if (ok)
183
+ return;
184
+ if (status < 500) {
185
+ throw new Error(`HTTP ${status} from ${url}`);
186
+ }
187
+ lastError = new Error(`HTTP ${status} from ${url}`);
188
+ if (status === 503) {
189
+ retryWaitMs = retryAfterWaitMs(retryAfter);
190
+ }
191
+ }
192
+ if (attempt < maxAttempts - 1) {
193
+ await sleep(retryWaitMs ?? baseDelayMs * 2 ** attempt);
194
+ }
195
+ }
196
+ throw (lastError ??
197
+ new Error(`postWithRetries exhausted ${maxAttempts} attempts but recorded no error`));
198
+ }
199
+ /**
200
+ * Runs one metadata-delivered tool call and exits. Not a server.
201
+ *
202
+ * Reads the ToolFunctionRequest envelope from the guest metadata directory,
203
+ * invokes the named tool from the registry, reports the ToolResultRequest
204
+ * to {oe_url}/tool/result, and returns. Mirrors Python's ToolFunctionRunner.
205
+ *
206
+ * `metadataDir` is injectable for tests.
207
+ */
208
+ export class ToolFunctionRunner {
209
+ runtime;
210
+ metadataDir;
211
+ constructor(runtime, { metadataDir = METADATA_DIR } = {}) {
212
+ this.runtime = runtime;
213
+ this.metadataDir = metadataDir;
214
+ }
215
+ /**
216
+ * Run one tool from the metadata-delivered request and report the result.
217
+ *
218
+ * Re-raises if the result POST cannot be delivered after retries so fctr
219
+ * records the execution as failed.
220
+ */
221
+ async run() {
222
+ const functionRequest = readFunctionRequest(this.metadataDir);
223
+ const { request, step } = functionRequest;
224
+ // Pin the callback base to the runner's own configured OE (deploy-time
225
+ // OE_URL) before it is used as the callback base or the owner-URL trust
226
+ // anchor. The request field is only honoured when no OE_URL is stamped
227
+ // (local dev / tests). Resolving once here means the owner URL is validated
228
+ // against a trusted anchor, not the raw request value.
229
+ const oeUrl = resolveOeUrl(request.oe_url ?? "").replace(/\/+$/, "");
230
+ if (!oeUrl) {
231
+ throw new Error("function mode requires oe_url on the request to report the result");
232
+ }
233
+ // Prefer the replica-specific owner OE, validated against the trusted
234
+ // resolved oeUrl above. A forged owner URL validates to null and is ignored.
235
+ const ownerUrl = resolveOwnerUrl(request.oe_owner_url, oeUrl);
236
+ const ownerUrlFailure = { failed: false };
237
+ // Install before ready()/registry prep and keep through result delivery so
238
+ // startup and callback failures still emit platform_trace_id.
239
+ await runWithExecutionContext({
240
+ executionId: request.execution_id,
241
+ traceId: request.platform_trace_id,
242
+ wrapper: null,
243
+ oeUrl,
244
+ oeOwnerUrl: ownerUrl,
245
+ ownerUrlFailure,
246
+ userId: request.user_id,
247
+ sessionId: request.session_id,
248
+ authorization: request.authorization,
249
+ customHeaders: request.custom_headers,
250
+ payload: request.payload,
251
+ }, async () => {
252
+ await this.runtime.graphBuilder?.ready?.();
253
+ // Populate the named-LLM registry so a tool calling an LLM works,
254
+ // same as ToolServer.onStartup(). Not required if no LLMs are called,
255
+ // but matches the server-mode startup contract.
256
+ this._prepare();
257
+ logger.info(`function mode: invoking tool=${JSON.stringify(request.tool_name)} execution_id=${request.execution_id} step=${step}`);
258
+ const started = performance.now();
259
+ const response = await this._invokeToolFn(request, ownerUrlFailure);
260
+ const durationMs = performance.now() - started;
261
+ const payload = buildResultPayload(buildResultRequest(request, response, step, durationMs), request.execution_id);
262
+ try {
263
+ await postWithRetries(`${oeUrl}${TOOL_RESULT_PATH}`, payload, ownerUrl && !ownerUrlFailure.failed
264
+ ? `${ownerUrl}${TOOL_RESULT_PATH}`
265
+ : null, () => {
266
+ ownerUrlFailure.failed = true;
267
+ });
268
+ }
269
+ catch (e) {
270
+ logger.error(`function mode: failed to deliver result for execution_id=${request.execution_id}: ${e.message}`);
271
+ throw e;
272
+ }
273
+ logger.info(`function mode: reported ${payload["status"]} result for execution_id=${request.execution_id} step=${step}`);
274
+ });
275
+ }
276
+ _prepare() {
277
+ populateLlmRegistryFromEntrypoint(this.runtime.graphBuilder, logger, {
278
+ entrypointFailed: "Failed to run entrypoint for LLM registration; LLM calls from this tool will fail",
279
+ noLlmRegistered: "Entrypoint ran but did not call app.llm(); LLM calls from this tool will fail",
280
+ });
281
+ }
282
+ async _invokeToolFn(request, ownerUrlFailure) {
283
+ const podName = os.hostname();
284
+ logToolRequest(request.tool_name ?? "(empty)", request.arguments, 0, "TOOL_FUNCTION", toolRedactFields(this.runtime.toolDefinitions, request.tool_name));
285
+ if (!request.tool_name) {
286
+ return {
287
+ status: "error",
288
+ error: "Tool name cannot be empty",
289
+ pod_name: podName,
290
+ metadata: {},
291
+ };
292
+ }
293
+ const func = this.runtime.tools[request.tool_name];
294
+ if (!func) {
295
+ return {
296
+ status: "error",
297
+ error: `Unknown tool: ${request.tool_name}`,
298
+ pod_name: podName,
299
+ metadata: {},
300
+ };
301
+ }
302
+ // Pin the callback base to the trusted resolved OE before it anchors the
303
+ // execution context and the owner-URL validation (see run()).
304
+ const oeUrl = resolveOeUrl(request.oe_url ?? "");
305
+ return runWithExecutionContext({
306
+ executionId: request.execution_id,
307
+ traceId: request.platform_trace_id,
308
+ wrapper: null,
309
+ oeUrl,
310
+ // Emit() from the tool prefers this owner replica, validated against
311
+ // the trusted resolved oeUrl above.
312
+ oeOwnerUrl: resolveOwnerUrl(request.oe_owner_url, oeUrl),
313
+ ownerUrlFailure,
314
+ userId: request.user_id,
315
+ sessionId: request.session_id,
316
+ authorization: request.authorization,
317
+ customHeaders: request.custom_headers,
318
+ payload: request.payload,
319
+ }, async () => {
320
+ try {
321
+ const maybePromise = runWithCustomerOrigin(() => func(request.arguments));
322
+ const result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
323
+ const metadata = getCurrentExecutionMetadata();
324
+ return {
325
+ status: "success",
326
+ result,
327
+ pod_name: podName,
328
+ kind: metadata["memory"] ? "memory" : undefined,
329
+ metadata,
330
+ };
331
+ }
332
+ catch (e) {
333
+ return executeErrorResponse(e, this.runtime.toolDefinitions[request.tool_name], podName, getCurrentExecutionMetadata(), requestCredentialValues());
334
+ }
335
+ });
336
+ }
337
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared retry-policy and bounded POST helpers for OE transports.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.http_retry`. The AER chunk/callback
5
+ * path (`server/aer.ts`), function mode (`server/function.ts`) and the tool
6
+ * result settlement loop (`secure_wrapper.ts`) share policy here so their
7
+ * cooperative back-pressure behaviour stays identical.
8
+ */
9
+ export declare function waitForRetry(ms: number, signal: AbortSignal): Promise<boolean>;
10
+ export declare class RetryablePostError extends Error {
11
+ }
12
+ export declare const RETRY_AFTER_MAX_WAIT_MS = 10000;
13
+ /** Release an unread fetch body without changing callback delivery semantics. */
14
+ export declare function discardResponseBody(response: Response): Promise<void>;
15
+ /**
16
+ * Parse a delta-seconds `Retry-After` header; ignore the HTTP-date form.
17
+ *
18
+ * Returns the non-negative integer seconds, or `null` when the header is
19
+ * absent or not in delta-seconds form (an HTTP-date, or garbage). Callers fall
20
+ * back to their normal backoff when this returns `null`.
21
+ */
22
+ export declare function parseRetryAfterSeconds(headerValue: string | null | undefined): number | null;
23
+ /**
24
+ * The wait (in ms) an honored delta-seconds `Retry-After` implies, capped at
25
+ * {@link RETRY_AFTER_MAX_WAIT_MS}, or `null` when the header is not honorable.
26
+ */
27
+ export declare function retryAfterWaitMs(headerValue: string | null | undefined): number | null;
28
+ /**
29
+ * POST to OE with one optional owner attempt and bounded service retries.
30
+ *
31
+ * Transport failures and 5xx responses are retried; 4xx responses fail
32
+ * immediately. An owner URL gets one best-effort pre-attempt without consuming
33
+ * the service retry budget. A 503 `Retry-After` delta overrides the normal
34
+ * exponential delay up to the configured cap.
35
+ */
36
+ export declare function postWithRetries(url: string, body: unknown, ownerUrl?: string | null, onOwnerFailure?: (() => void) | null, cancellationSignal?: AbortSignal, maxAttempts?: number, baseDelayMs?: number, shouldAttempt?: () => boolean): Promise<void>;
37
+ //# sourceMappingURL=http_retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http_retry.d.ts","sourceRoot":"","sources":["../../src/server/http_retry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;CAAG;AAKhD,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,iFAAiF;AACjF,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,IAAI,CAOf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EACxB,cAAc,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,EACpC,kBAAkB,CAAC,EAAE,WAAW,EAChC,WAAW,SAAwB,EACnC,WAAW,SAA6B,EACxC,aAAa,CAAC,EAAE,MAAM,OAAO,GAC5B,OAAO,CAAC,IAAI,CAAC,CA6Df"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Shared retry-policy and bounded POST helpers for OE transports.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.http_retry`. The AER chunk/callback
5
+ * path (`server/aer.ts`), function mode (`server/function.ts`) and the tool
6
+ * result settlement loop (`secure_wrapper.ts`) share policy here so their
7
+ * cooperative back-pressure behaviour stays identical.
8
+ */
9
+ import { getLogger } from "../logger.js";
10
+ import { ownerDelivered as ownerPreAttemptDelivered } from "../owner_callback.js";
11
+ import { fetchPlatform, getFetchOptionsWithTLS } from "../tls_client.js";
12
+ import { getRequestTimeout } from "../utils.js";
13
+ const logger = getLogger("agent_engine_runner_shared.server.http_retry");
14
+ const REQUEST_TIMEOUT_MS = getRequestTimeout() * 1000;
15
+ const DEFAULT_POST_ATTEMPTS = 3;
16
+ const DEFAULT_POST_BASE_DELAY_MS = 100;
17
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+ export function waitForRetry(ms, signal) {
19
+ return new Promise((resolve) => {
20
+ if (signal.aborted) {
21
+ resolve(false);
22
+ return;
23
+ }
24
+ const onAbort = () => {
25
+ clearTimeout(timer);
26
+ resolve(false);
27
+ };
28
+ const timer = setTimeout(() => {
29
+ signal.removeEventListener("abort", onAbort);
30
+ resolve(true);
31
+ }, ms);
32
+ signal.addEventListener("abort", onAbort, { once: true });
33
+ });
34
+ }
35
+ export class RetryablePostError extends Error {
36
+ }
37
+ // Ceiling on an honored `Retry-After` wait: a cooperative 503 hint is worth a
38
+ // short pause, but the runner must not park a callback for an OE-chosen
39
+ // duration.
40
+ export const RETRY_AFTER_MAX_WAIT_MS = 10_000;
41
+ /** Release an unread fetch body without changing callback delivery semantics. */
42
+ export async function discardResponseBody(response) {
43
+ try {
44
+ await response.body?.cancel();
45
+ }
46
+ catch {
47
+ // The response outcome is authoritative; cleanup failure must not replace it.
48
+ }
49
+ }
50
+ /**
51
+ * Parse a delta-seconds `Retry-After` header; ignore the HTTP-date form.
52
+ *
53
+ * Returns the non-negative integer seconds, or `null` when the header is
54
+ * absent or not in delta-seconds form (an HTTP-date, or garbage). Callers fall
55
+ * back to their normal backoff when this returns `null`.
56
+ */
57
+ export function parseRetryAfterSeconds(headerValue) {
58
+ if (headerValue == null)
59
+ return null;
60
+ const value = headerValue.trim();
61
+ // Only ASCII digits (matches Python's `str.isdigit()`): no sign, decimal, or
62
+ // HTTP-date. An empty string fails, so a blank header falls through.
63
+ if (!/^\d+$/.test(value))
64
+ return null;
65
+ return Number(value);
66
+ }
67
+ /**
68
+ * The wait (in ms) an honored delta-seconds `Retry-After` implies, capped at
69
+ * {@link RETRY_AFTER_MAX_WAIT_MS}, or `null` when the header is not honorable.
70
+ */
71
+ export function retryAfterWaitMs(headerValue) {
72
+ const seconds = parseRetryAfterSeconds(headerValue);
73
+ if (seconds === null)
74
+ return null;
75
+ return Math.min(seconds * 1000, RETRY_AFTER_MAX_WAIT_MS);
76
+ }
77
+ /**
78
+ * POST to OE with one optional owner attempt and bounded service retries.
79
+ *
80
+ * Transport failures and 5xx responses are retried; 4xx responses fail
81
+ * immediately. An owner URL gets one best-effort pre-attempt without consuming
82
+ * the service retry budget. A 503 `Retry-After` delta overrides the normal
83
+ * exponential delay up to the configured cap.
84
+ */
85
+ export async function postWithRetries(url, body, ownerUrl, onOwnerFailure, cancellationSignal, maxAttempts = DEFAULT_POST_ATTEMPTS, baseDelayMs = DEFAULT_POST_BASE_DELAY_MS, shouldAttempt) {
86
+ const serialized = JSON.stringify(body);
87
+ if (shouldAttempt?.() === false)
88
+ return;
89
+ if (ownerUrl != null) {
90
+ if (await ownerDelivered(ownerUrl, url, serialized))
91
+ return;
92
+ onOwnerFailure?.();
93
+ }
94
+ let lastError = null;
95
+ const tlsOptions = getFetchOptionsWithTLS(url);
96
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
97
+ if (shouldAttempt?.() === false)
98
+ return;
99
+ let retryWaitMs = null;
100
+ let response;
101
+ try {
102
+ const requestSignal = cancellationSignal
103
+ ? AbortSignal.any([
104
+ cancellationSignal,
105
+ AbortSignal.timeout(REQUEST_TIMEOUT_MS),
106
+ ])
107
+ : AbortSignal.timeout(REQUEST_TIMEOUT_MS);
108
+ response = await fetchPlatform(url, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: serialized,
112
+ signal: requestSignal,
113
+ ...tlsOptions,
114
+ });
115
+ }
116
+ catch (error) {
117
+ if (cancellationSignal?.aborted)
118
+ throw error;
119
+ lastError = new RetryablePostError(error instanceof Error ? error.message : String(error));
120
+ }
121
+ if (response) {
122
+ const ok = response.ok;
123
+ const status = response.status;
124
+ const retryAfter = response.headers?.get?.("Retry-After") ?? null;
125
+ await discardResponseBody(response);
126
+ if (ok)
127
+ return;
128
+ if (status < 500)
129
+ throw new Error(`HTTP ${status} from ${url}`);
130
+ lastError = new RetryablePostError(`HTTP ${status} from ${url}`);
131
+ if (status === 503)
132
+ retryWaitMs = retryAfterWaitMs(retryAfter);
133
+ }
134
+ if (attempt < maxAttempts - 1) {
135
+ const delayMs = retryWaitMs ?? baseDelayMs * 2 ** attempt;
136
+ if (cancellationSignal) {
137
+ if (!(await waitForRetry(delayMs, cancellationSignal))) {
138
+ throw cancellationSignal.reason;
139
+ }
140
+ }
141
+ else {
142
+ await sleep(delayMs);
143
+ }
144
+ }
145
+ }
146
+ throw (lastError ??
147
+ new Error(`postWithRetries exhausted ${maxAttempts} attempts but recorded no error`));
148
+ }
149
+ async function ownerDelivered(ownerUrl, serviceUrl, serialized) {
150
+ return ownerPreAttemptDelivered(ownerUrl, () => ({
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ body: serialized,
154
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
155
+ ...getFetchOptionsWithTLS(ownerUrl),
156
+ }), (message) => logger.warn(`${message} URL ${serviceUrl}`));
157
+ }
@@ -0,0 +1,7 @@
1
+ export * as chunkTypes from "./chunk_types.js";
2
+ export * from "./base.js";
3
+ export { AERServer } from "./aer.js";
4
+ export { ToolServer, LLMRegistryLoadError } from "./tool.js";
5
+ export { ToolFunctionRunner, ToolFunctionRequestSchema, buildResultPayload, type ToolFunctionRequest, } from "./function.js";
6
+ export type { AERQueryPlugin } from "./query.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAC/C,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * as chunkTypes from "./chunk_types.js";
2
+ export * from "./base.js";
3
+ export { AERServer } from "./aer.js";
4
+ export { ToolServer, LLMRegistryLoadError } from "./tool.js";
5
+ export { ToolFunctionRunner, ToolFunctionRequestSchema, buildResultPayload, } from "./function.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Read request-scoped secrets from the fctr metadata directory.
3
+ *
4
+ * In RPC mode the OE delivers per-tool secrets via `SetMetadata` before each
5
+ * tool call and clears them after (when restriction is enabled). fctr
6
+ * materializes the keyset as files at `/run/meta/<KEY>`. This module reads
7
+ * those files so the tool server can inject them into `process.env` for the
8
+ * duration of the call — or permanently when restriction is disabled.
9
+ *
10
+ * Mirrors Python's `agent_engine_runner_shared.server.metadata`.
11
+ */
12
+ /**
13
+ * Read all key-value pairs from the metadata directory.
14
+ *
15
+ * Returns a record mapping filename (the secret name) to file content (the
16
+ * secret value). Skips files that cannot be read and logs a warning. Returns
17
+ * an empty record when the directory does not exist.
18
+ */
19
+ export declare function readMetadataSecrets(metadataDir?: string): Record<string, string>;
20
+ /**
21
+ * Overwrite `process.env` from `/run/meta`; no restore or prune.
22
+ */
23
+ export declare function mergeMetadataEnv(metadataDir?: string): void;
24
+ /**
25
+ * Run `fn` with metadata secrets merged into `process.env`, restoring the
26
+ * environment afterwards (even on throw).
27
+ *
28
+ * Mirrors Python's `applied_metadata_env` context manager for non-streaming
29
+ * call paths.
30
+ *
31
+ * @throws if this region overlaps a concurrent secret-bearing region; see
32
+ * {@link applyMetadataEnv} for the fail-closed rule and the clean-vs-tainted
33
+ * restore semantics.
34
+ */
35
+ export declare function withMetadataEnv<T>(fn: () => Promise<T>, metadataDir?: string): Promise<T>;
36
+ /**
37
+ * Wrap an async generator so metadata secrets are present in `process.env`
38
+ * for the lifetime of the iteration, restoring the environment when iteration
39
+ * completes, throws, or is closed early.
40
+ *
41
+ * Mirrors Python's `applied_metadata_env` context manager for streaming call
42
+ * paths.
43
+ *
44
+ * @throws if this region overlaps a concurrent secret-bearing region — note a
45
+ * stream holds its region open for the whole iteration; see
46
+ * {@link applyMetadataEnv} for the fail-closed rule and the clean-vs-tainted
47
+ * restore semantics.
48
+ */
49
+ export declare function withMetadataEnvGen<T>(genFactory: () => AsyncGenerator<T>, metadataDir?: string): AsyncGenerator<T>;
50
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/server/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAwBH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,GAAE,MAAqB,GACjC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAqBxB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,GAAE,MAAqB,GAAG,IAAI,CAQzE;AAwFD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,WAAW,GAAE,MAAqB,GACjC,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAuB,kBAAkB,CAAC,CAAC,EACzC,UAAU,EAAE,MAAM,cAAc,CAAC,CAAC,CAAC,EACnC,WAAW,GAAE,MAAqB,GACjC,cAAc,CAAC,CAAC,CAAC,CAOnB"}