@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
package/dist/utils.js ADDED
@@ -0,0 +1,867 @@
1
+ /**
2
+ * Utility functions for Runner SDK.
3
+ */
4
+ import { getLogger } from "./logger.js";
5
+ // Module-level logger — equivalent to Python `logging.getLogger(__name__)`.
6
+ const logger = getLogger("agent_engine_runner_shared.utils");
7
+ // =============================================================================
8
+ // Runtime Mode
9
+ // =============================================================================
10
+ /** Runtime mode for the Runner SDK. */
11
+ export var RuntimeMode;
12
+ (function (RuntimeMode) {
13
+ RuntimeMode["AER"] = "aer";
14
+ RuntimeMode["TOOL"] = "tool";
15
+ // Tool role, per-call (function) lifecycle: the process boots, runs a
16
+ // single named tool from the registry, reports the result, and exits.
17
+ // TOOL is the same role as a long-running server handling many /execute
18
+ // calls. The value carries the role because the function lifecycle is
19
+ // orthogonal to it (a future per-call AER would be e.g. "aer_function").
20
+ // The invocation is read once from the guest metadata channel, not env.
21
+ RuntimeMode["TOOL_FUNCTION"] = "tool_function";
22
+ })(RuntimeMode || (RuntimeMode = {}));
23
+ const MODE_MAP = {
24
+ [RuntimeMode.AER]: RuntimeMode.AER,
25
+ [RuntimeMode.TOOL]: RuntimeMode.TOOL,
26
+ [RuntimeMode.TOOL_FUNCTION]: RuntimeMode.TOOL_FUNCTION,
27
+ };
28
+ /**
29
+ * Get the current runtime mode from environment variable.
30
+ *
31
+ * @returns RuntimeMode based on RUNNER_MODE environment variable.
32
+ * @throws Error If RUNNER_MODE is not set or is not a valid mode.
33
+ */
34
+ export function getRuntimeMode() {
35
+ const raw = process.env["RUNNER_MODE"];
36
+ if (raw === undefined) {
37
+ throw new Error(`RUNNER_MODE environment variable is required. Valid values: ${Object.keys(MODE_MAP).join(", ")}`);
38
+ }
39
+ const mode = raw.toLowerCase();
40
+ const resolved = MODE_MAP[mode];
41
+ if (!resolved) {
42
+ throw new Error(`Unknown RUNNER_MODE '${mode}'. Valid values: ${Object.keys(MODE_MAP).join(", ")}`);
43
+ }
44
+ return resolved;
45
+ }
46
+ // =============================================================================
47
+ // Platform-owned environment variables
48
+ // =============================================================================
49
+ //
50
+ // Used by `agent_config.ts` to validate fields that name an env var (e.g.
51
+ // `mcp.servers.*.auth.token_env`) and to build the substitution mapping for
52
+ // `loadRuntimeAgentConfig(envVars=...)` so tenant `agent.yaml` cannot use
53
+ // `${VAR}` interpolation to dereference platform secrets or internal service
54
+ // addresses (e.g. `url: https://attacker/${OPENAI_API_KEY}`).
55
+ //
56
+ // Maintenance: whenever a new platform-owned env var is introduced in
57
+ // `agent-engine-runner-shared` (or a sibling package whose env reaches the runtime
58
+ // container), add its exact name to `PLATFORM_ENV_VARS` or, if it shares a
59
+ // common prefix with related platform vars, extend
60
+ // `PLATFORM_ENV_VAR_PREFIXES`. The prefix list does most of the work — prefer
61
+ // adding a prefix over enumerating individual names. Keep in sync with
62
+ // `agent_engine_runner_shared/utils.py`'s `_PLATFORM_ENV_VAR_PREFIXES` / `_PLATFORM_ENV_VARS`.
63
+ const PLATFORM_ENV_VAR_PREFIXES = [
64
+ // `AGENTIC_*` is the platform's reserved namespace. Tenants are documented
65
+ // to use unprefixed names for their own vars.
66
+ "AGENTIC_",
67
+ // Component-scoped families
68
+ "MONGOMEM_", // memory server config
69
+ "MONGODB_", // MongoDB URI + client tunables
70
+ "VOYAGE_", // platform-owned embedder config
71
+ "LLM_", // retry tunables in this module
72
+ "OE_", // orchestration engine
73
+ "AER_", // Agent Execution Runtime endpoints
74
+ "TOOL_", // Tool Pod endpoints
75
+ "ECP_", // Executor Control Plane
76
+ "RUNNER_", // runner-internal tunables (mode, stream timeouts, ...)
77
+ "GUARDRAILS_", // guardrails server config + LLM key
78
+ "FILESYSTEM_", // Tool Pod sandbox limits
79
+ "SHELL_", // Tool Pod sandbox limits
80
+ "MAX_", // Tool Pod sandbox limits (MAX_LS_ENTRIES, MAX_GREP_*, ...)
81
+ // Auth / observability
82
+ "OKTA_",
83
+ "OIDC_",
84
+ "OTEL_",
85
+ // Test infra (must not leak into production tenant interpolation either)
86
+ "E2E_",
87
+ // Container / orchestration infra
88
+ "KUBERNETES_",
89
+ "HELIX_",
90
+ // POSIX
91
+ "LC_",
92
+ ];
93
+ const PLATFORM_ENV_VARS = new Set([
94
+ // Multi-tenant identity
95
+ "ORG_ID",
96
+ "PROJECT_ID",
97
+ "ATLAS_GROUP_ID",
98
+ "TENANT_ID",
99
+ "APP_ID",
100
+ // Operator-stamped MaaS observability identity (keep in sync with
101
+ // utils.py's `_PLATFORM_ENV_VARS`).
102
+ "AGENT_ENGINE_ENVIRONMENT",
103
+ // Platform LLM provider credentials. Read by the standalone memory server
104
+ // for extraction-pipeline provider detection, but potentially present in
105
+ // any runtime container.
106
+ "OPENAI_API_KEY",
107
+ "ANTHROPIC_API_KEY",
108
+ "CEREBRAS_API_KEY",
109
+ "GEMINI_API_KEY",
110
+ // Platform LLM endpoints (not secret, but leaking them via tenant YAML is
111
+ // still pointless and a potential SSRF vector).
112
+ "OPENAI_BASE_URL",
113
+ "ANTHROPIC_BASE_URL",
114
+ // Auth / inter-service secrets
115
+ "A2A_JWT_SECRET",
116
+ // TLS / mTLS certificate paths (operator-mounted for AER→OE HTTPS)
117
+ "TLS_CERT_PATH",
118
+ "TLS_KEY_PATH",
119
+ "TLS_CA_CERT_PATH",
120
+ // TLS / mTLS PEM content (VM mode via SecretKeyRef, kubelet decodes automatically)
121
+ // CRITICAL: Must be excluded to prevent tenant exfiltration via ${TLS_KEY_PEM}
122
+ "TLS_CERT_PEM",
123
+ "TLS_KEY_PEM",
124
+ "TLS_CA_CERT_PEM",
125
+ // Server / network config
126
+ "APP_HOST",
127
+ "APP_PORT",
128
+ "LOG_LEVEL",
129
+ "LOG_DIR", // operator-injected via render.WorkloadInjectedEnv
130
+ "STRUCTURED_LOGGING", // operator-injected via render.StructuredLoggingEnv
131
+ "CORS_ALLOWED_ORIGINS", // OE-injected (forward-defensive on AER)
132
+ "USE_MEMORY_CLIENT",
133
+ "AGENT_STREAM_TERMINAL_DRAIN_TIMEOUT",
134
+ "SHUTDOWN_GRACE_PERIOD_MS",
135
+ // Runner launcher entrypoint baked into the Dockerfile ENV by ECP
136
+ "AGENT_ENTRYPOINT",
137
+ // Platform persistence
138
+ "PLATFORM_DATABASE",
139
+ "MEMORY_DATABASE_NAME",
140
+ "MDB_AGENTIC_STORE_DB",
141
+ "CHECKPOINT_DB_NAME",
142
+ "DB_NAME",
143
+ "CHECKPOINTER_SERVER_SELECTION_TIMEOUT",
144
+ "CHECKPOINTER_CONNECT_TIMEOUT",
145
+ "CHECKPOINTER_SOCKET_TIMEOUT",
146
+ "TEST_MONGO_URI",
147
+ // Tool Pod sandbox (covered partially by prefix, but the no-prefix
148
+ // `WORKSPACE_DIR` / `DOWNLOAD_MAX_BYTES` names live here).
149
+ "WORKSPACE_DIR",
150
+ "DOWNLOAD_MAX_BYTES",
151
+ // Feature toggles
152
+ "ENABLE_MEMORY",
153
+ "ENABLE_TRACING",
154
+ "ENABLE_VECTOR_SEARCH_TESTS",
155
+ // Dev / test infra
156
+ "SEED_DATA",
157
+ "CI",
158
+ // Container / pod infra
159
+ "POD_NAME",
160
+ "HOSTNAME",
161
+ // Generic POSIX. Substituting these into a URL is never what the tenant
162
+ // means; blocking prevents accidental leakage of process identity /
163
+ // filesystem layout.
164
+ "PATH",
165
+ "HOME",
166
+ "USER",
167
+ "PWD",
168
+ "LANG",
169
+ "TERM",
170
+ "SHELL",
171
+ "SHLVL",
172
+ "_",
173
+ ]);
174
+ /**
175
+ * Return `true` if `name` matches a platform-owned env var.
176
+ *
177
+ * Public-API view of the same membership check used by `tenantEnvVars`.
178
+ */
179
+ export function isPlatformEnvVar(name) {
180
+ return (PLATFORM_ENV_VARS.has(name) ||
181
+ PLATFORM_ENV_VAR_PREFIXES.some((prefix) => name.startsWith(prefix)));
182
+ }
183
+ /**
184
+ * Return the tenant-owned subset of environment variables.
185
+ *
186
+ * `source` defaults to `process.env`. Names in `PLATFORM_ENV_VARS` or
187
+ * matching any prefix in `PLATFORM_ENV_VAR_PREFIXES` are excluded, so the
188
+ * result is safe to pass as the substitution mapping to
189
+ * `loadRuntimeAgentConfig({ envVars: ... })`.
190
+ */
191
+ export function tenantEnvVars(source) {
192
+ const from = source ?? process.env;
193
+ const result = {};
194
+ for (const [name, value] of Object.entries(from)) {
195
+ if (value !== undefined && !isPlatformEnvVar(name)) {
196
+ result[name] = value;
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+ // =============================================================================
202
+ // Environment Helpers
203
+ // =============================================================================
204
+ /** Get environment variable with default. */
205
+ export function getEnv(name, defaultValue = "") {
206
+ return process.env[name] ?? defaultValue;
207
+ }
208
+ /** Get integer environment variable with default. */
209
+ export function getEnvInt(name, defaultValue) {
210
+ const val = process.env[name];
211
+ if (!val)
212
+ return defaultValue;
213
+ const parsed = parseInt(val, 10);
214
+ return isNaN(parsed) ? defaultValue : parsed;
215
+ }
216
+ /** Get float environment variable with default. */
217
+ export function getEnvFloat(name, defaultValue) {
218
+ const val = process.env[name];
219
+ if (!val)
220
+ return defaultValue;
221
+ const parsed = parseFloat(val);
222
+ return isNaN(parsed) ? defaultValue : parsed;
223
+ }
224
+ /** Get boolean environment variable with default. */
225
+ export function getEnvBool(name, defaultValue = false) {
226
+ const raw = process.env[name];
227
+ const value = (raw ?? String(defaultValue)).toLowerCase();
228
+ return ["true", "1", "yes", "on"].includes(value);
229
+ }
230
+ /**
231
+ * Get the HTTP request timeout from environment.
232
+ *
233
+ * Uses RUNNER_REQUEST_TIMEOUT env var, defaults to 60.0 seconds.
234
+ */
235
+ export function getRequestTimeout() {
236
+ return getEnvFloat("RUNNER_REQUEST_TIMEOUT", 60.0);
237
+ }
238
+ /**
239
+ * Get the tool-call read timeout from environment.
240
+ *
241
+ * The OE holds /tool/execute open until the tool result comes back, so this
242
+ * bounds the tool's own runtime rather than the handshake. It matches the OE's
243
+ * own tool deadline: a smaller value abandons a tool the platform is still
244
+ * happily running, leaving the SDK with no result to report.
245
+ *
246
+ * Uses RUNNER_TOOL_READ_TIMEOUT env var, defaults to 600.0 seconds.
247
+ */
248
+ export function getToolReadTimeout() {
249
+ return getEnvFloat("RUNNER_TOOL_READ_TIMEOUT", 600.0);
250
+ }
251
+ // =============================================================================
252
+ // LLM Retry Configuration
253
+ // =============================================================================
254
+ // LLM retry configuration for rate limit errors.
255
+ export const LLM_MAX_RETRIES = getEnvInt("LLM_MAX_RETRIES", 3);
256
+ export const LLM_INITIAL_BACKOFF = getEnvFloat("LLM_INITIAL_BACKOFF", 1.0); // seconds
257
+ export const LLM_BACKOFF_MULTIPLIER = getEnvFloat("LLM_BACKOFF_MULTIPLIER", 2.0);
258
+ export const LLM_MAX_BACKOFF = getEnvFloat("LLM_MAX_BACKOFF", 30.0); // seconds
259
+ /** Same-step retries of /tool/execute (and the SSE relay) after OE advertises retryable=true, and after an SSE transport disconnect. 1 initial + 2 extras; lockstep with Python. */
260
+ export const OE_RETRYABLE_MAX_ATTEMPTS = 3;
261
+ /**
262
+ * Wait after a dropped SSE connection so the next attempt can land after
263
+ * dispatch_heartbeat TTL (30s). Do not reuse HTTP Retry-After's 10s cap.
264
+ * Lockstep with Python.
265
+ */
266
+ export const OE_DISPATCH_TAKEOVER_RETRY_DELAY_MS = 30_000;
267
+ export const OE_DISPATCH_RETRY_MAX_WAIT_MS = 60_000;
268
+ /** Honor a server-provided SSE retry_after_ms, capped. Absent means retry immediately. */
269
+ export function oeStreamRetryDelayMs(retryAfterMs) {
270
+ if (retryAfterMs == null ||
271
+ !Number.isFinite(retryAfterMs) ||
272
+ retryAfterMs <= 0) {
273
+ return 0;
274
+ }
275
+ return Math.min(retryAfterMs, OE_DISPATCH_RETRY_MAX_WAIT_MS);
276
+ }
277
+ /** Sleep before an SSE same-URL retry. Tests spy this to avoid wall-clock waits. */
278
+ export const oeStreamRetry = {
279
+ async sleep(delayMs) {
280
+ if (delayMs <= 0)
281
+ return;
282
+ await new Promise((resolve) => {
283
+ setTimeout(resolve, delayMs);
284
+ });
285
+ },
286
+ };
287
+ // LLM read timeout — LLM generation can take much longer than a typical HTTP request.
288
+ // Applies to both streaming (per-chunk wait) and non-streaming (full-response wait) paths.
289
+ export const LLM_READ_TIMEOUT = getEnvFloat("RUNNER_LLM_READ_TIMEOUT", getEnvFloat("RUNNER_STREAM_READ_TIMEOUT", 300.0)); // seconds
290
+ /** Classify provider failures before any LLM output has been exposed. */
291
+ export function isRetryableError(error) {
292
+ const seen = new Set();
293
+ let current = error;
294
+ let retryable = false;
295
+ const messages = [];
296
+ while (current !== undefined && current !== null && !seen.has(current)) {
297
+ seen.add(current);
298
+ if (typeof current !== "object") {
299
+ messages.push(String(current).toLowerCase());
300
+ break;
301
+ }
302
+ const candidate = current;
303
+ const errorType = current instanceof Error ? current.constructor.name : undefined;
304
+ if (candidate["name"] === "AbortError" ||
305
+ errorType === "APIUserAbortError") {
306
+ return false;
307
+ }
308
+ const status = exceptionHttpStatus(current);
309
+ if (status !== undefined) {
310
+ // A structured rejection must not be overridden by error body text.
311
+ if (![408, 409, 429].includes(status) && !(status >= 500 && status < 600))
312
+ return false;
313
+ retryable = true;
314
+ }
315
+ // Provider SDKs wrap transport exceptions, sometimes without a cause.
316
+ if (candidate["name"] === "TimeoutError" ||
317
+ errorType === "APIConnectionError" ||
318
+ errorType === "APIConnectionTimeoutError")
319
+ retryable = true;
320
+ if ([
321
+ "ETIMEDOUT",
322
+ "ECONNRESET",
323
+ "ECONNREFUSED",
324
+ "ECONNABORTED",
325
+ "EPIPE",
326
+ "UND_ERR_CONNECT_TIMEOUT",
327
+ "UND_ERR_HEADERS_TIMEOUT",
328
+ "UND_ERR_BODY_TIMEOUT",
329
+ "UND_ERR_SOCKET",
330
+ "server_error",
331
+ "rate_limit_exceeded",
332
+ "too_many_requests",
333
+ "overloaded_error",
334
+ ].includes(String(candidate["code"])))
335
+ retryable = true;
336
+ if (["server_error", "overloaded_error"].includes(String(candidate["type"]))) {
337
+ retryable = true;
338
+ }
339
+ messages.push(String(current).toLowerCase());
340
+ current = candidate["cause"];
341
+ }
342
+ // Retain support for adapters that expose only an unstructured exception.
343
+ const retryablePatterns = [
344
+ "too_many_requests",
345
+ "rate_limit",
346
+ "too many requests",
347
+ "rate limit exceeded",
348
+ "internal server error",
349
+ "service unavailable",
350
+ "gateway timeout",
351
+ "server error",
352
+ "queue_exceeded",
353
+ "high traffic",
354
+ "overloaded",
355
+ "upstream connect error or disconnect/reset before headers",
356
+ "the server had an error processing your request",
357
+ ];
358
+ // Require status context: an arbitrary token count or request ID is not an HTTP failure.
359
+ const statusPattern = /\b(?:http(?: status)?|status(?: code)?|error code)[:=]?\s*(?:408|409|429|5\d\d)\b/;
360
+ return (retryable ||
361
+ messages.some((message) => retryablePatterns.some((pattern) => message.includes(pattern)) ||
362
+ statusPattern.test(message)));
363
+ }
364
+ /**
365
+ * Render an LLM provider error for display, without escaped-JSON text.
366
+ *
367
+ * Some provider SDKs attach the raw API error body as an object on the
368
+ * error or its `cause` (`.details`, `.body`). That object's values are
369
+ * often themselves JSON-encoded strings containing real newlines (e.g. a
370
+ * pretty-printed nested error payload), so `String()`/template-literal
371
+ * stringification ends up producing a repr that turns the newlines into
372
+ * literal `\n` sequences. Decode any JSON-encoded string values
373
+ * first and re-serialize with `JSON.stringify(..., null, 2)` so the result
374
+ * renders as readable, indented JSON instead.
375
+ */
376
+ export function formatLlmError(error) {
377
+ const candidates = [error, error instanceof Error ? error.cause : undefined];
378
+ for (const candidate of candidates) {
379
+ if (candidate === null || candidate === undefined)
380
+ continue;
381
+ const c = candidate;
382
+ try {
383
+ let body = c["details"];
384
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
385
+ body = c["body"];
386
+ }
387
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) {
388
+ return JSON.stringify(decodeNestedJson(body), jsonReplacer, 2);
389
+ }
390
+ }
391
+ catch {
392
+ // A provider-supplied .details/.body can be anything -- a plain
393
+ // property, a getter that throws, a dict with circular refs or exotic
394
+ // values. Never let formatting itself throw — fall back to the plain
395
+ // message below.
396
+ break;
397
+ }
398
+ }
399
+ return error instanceof Error ? error.message : String(error);
400
+ }
401
+ /** `JSON.stringify` replacer that stringifies values it can't serialize natively. */
402
+ function jsonReplacer(_key, value) {
403
+ if (typeof value === "bigint" ||
404
+ typeof value === "function" ||
405
+ typeof value === "symbol") {
406
+ return String(value);
407
+ }
408
+ return value;
409
+ }
410
+ /** Recursively `JSON.parse()` any string value that looks like JSON. */
411
+ function decodeNestedJson(value) {
412
+ if (typeof value === "string") {
413
+ const stripped = value.trim();
414
+ if (stripped && (stripped[0] === "{" || stripped[0] === "[")) {
415
+ try {
416
+ return decodeNestedJson(JSON.parse(stripped));
417
+ }
418
+ catch {
419
+ return value;
420
+ }
421
+ }
422
+ return value;
423
+ }
424
+ if (Array.isArray(value)) {
425
+ return value.map(decodeNestedJson);
426
+ }
427
+ if (typeof value === "object" && value !== null) {
428
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [
429
+ k,
430
+ decodeNestedJson(v),
431
+ ]));
432
+ }
433
+ return value;
434
+ }
435
+ /**
436
+ * True when an LLM provider error is an auth rejection (HTTP 401/403).
437
+ *
438
+ * Reads only the provider SDK's own status fields — `status` (openai-node /
439
+ * anthropic-node style), `status_code`, `response.status` /
440
+ * `response.status_code` (fetch/axios style), and an integer `code` —
441
+ * walking the `cause` chain because adapters occasionally re-throw. Text is
442
+ * never matched: the caller maps a positive result onto the wire-level
443
+ * credential error code, and anything unrecognized returns false so the
444
+ * failure keeps its existing generic classification.
445
+ */
446
+ export function isLlmCredentialRejection(error) {
447
+ const seen = new Set();
448
+ const stack = [error];
449
+ while (stack.length > 0) {
450
+ const current = stack.pop();
451
+ if (current === null || current === undefined || seen.has(current)) {
452
+ continue;
453
+ }
454
+ seen.add(current);
455
+ const status = exceptionHttpStatus(current);
456
+ if (status === 401 || status === 403) {
457
+ return true;
458
+ }
459
+ if (current instanceof Error) {
460
+ stack.push(current.cause);
461
+ }
462
+ }
463
+ return false;
464
+ }
465
+ /** Extract the HTTP status a provider SDK attached to its error. */
466
+ function exceptionHttpStatus(error) {
467
+ if (typeof error !== "object" || error === null) {
468
+ return undefined;
469
+ }
470
+ const e = error;
471
+ const response = e["response"];
472
+ const responseStatus = typeof response === "object" && response !== null
473
+ ? (response["status"] ??
474
+ response["status_code"])
475
+ : undefined;
476
+ for (const candidate of [
477
+ e["status"],
478
+ e["status_code"],
479
+ responseStatus,
480
+ e["code"],
481
+ ]) {
482
+ if (typeof candidate === "number" &&
483
+ Number.isInteger(candidate) &&
484
+ candidate >= 100 &&
485
+ candidate < 600) {
486
+ return candidate;
487
+ }
488
+ }
489
+ return undefined;
490
+ }
491
+ // =============================================================================
492
+ // Content Normalization
493
+ // =============================================================================
494
+ /**
495
+ * Normalize LLM message content to a string.
496
+ *
497
+ * LLM content can be:
498
+ * - A string (normal text)
499
+ * - A list (multimodal content with text and other parts)
500
+ * - null/undefined or empty
501
+ *
502
+ * This handles cases where LangChain's AIMessageChunk.content is a list
503
+ * of content blocks (e.g., from Gemini multimodal responses) rather than
504
+ * a simple string.
505
+ *
506
+ * @param content The content to normalize (string, list, or null/undefined)
507
+ * @returns A string representation of the content
508
+ */
509
+ export function normalizeContent(content) {
510
+ if (content === null || content === undefined)
511
+ return "";
512
+ if (typeof content === "string")
513
+ return content;
514
+ if (Array.isArray(content)) {
515
+ const parts = [];
516
+ for (const part of content) {
517
+ if (typeof part === "string") {
518
+ parts.push(part);
519
+ }
520
+ else if (typeof part === "object" && part !== null) {
521
+ const p = part;
522
+ if (p["type"] === "text" && typeof p["text"] === "string") {
523
+ parts.push(p["text"]);
524
+ }
525
+ }
526
+ }
527
+ return parts.join("");
528
+ }
529
+ return String(content);
530
+ }
531
+ /** Normalize streamed tool-call args into the wire-format string payload. */
532
+ export function normalizeToolCallArgs(args) {
533
+ if (args === null || args === undefined)
534
+ return null;
535
+ if (typeof args === "string")
536
+ return args;
537
+ try {
538
+ return JSON.stringify(args);
539
+ }
540
+ catch {
541
+ return String(args);
542
+ }
543
+ }
544
+ /**
545
+ * Trim a string tool-metadata value, collapsing blank/non-string to null.
546
+ *
547
+ * Shared by the framework SDKs' tool-wrapping code so metadata values written
548
+ * as "" or " " are treated the same as absent rather
549
+ * than sent to the OE as a non-empty-looking but meaningless string.
550
+ */
551
+ export function normalizeOptionalStr(value) {
552
+ if (typeof value === "string") {
553
+ const trimmed = value.trim();
554
+ if (trimmed.length > 0)
555
+ return trimmed;
556
+ }
557
+ return null;
558
+ }
559
+ // =============================================================================
560
+ // Thinking Token Filter
561
+ // =============================================================================
562
+ const THINK_OPEN = "<think>";
563
+ const THINK_CLOSE = "</think>";
564
+ /**
565
+ * Length of the longest suffix of *buffer* that is a *proper* prefix of
566
+ * `<think>` (i.e. `<`, `<t`, … `<think` but never the full tag — a full
567
+ * match is handled by `indexOf`). Used to hold back a marker that was split
568
+ * across streamed chunks so its tail can't leak as plain text. Returns 0
569
+ * when no partial opener trails the buffer.
570
+ */
571
+ function partialOpenSuffixLength(buffer) {
572
+ const max = Math.min(buffer.length, THINK_OPEN.length - 1);
573
+ for (let k = max; k > 0; k--) {
574
+ if (buffer.endsWith(THINK_OPEN.slice(0, k)))
575
+ return k;
576
+ }
577
+ return 0;
578
+ }
579
+ /** Remove `<think>...</think>` blocks and unclosed `<think>` tails. */
580
+ export function stripThinking(text) {
581
+ if (!text)
582
+ return "";
583
+ let cleaned = text.replace(/<think>.*?<\/think>/gs, "");
584
+ cleaned = cleaned.replace(/<think>.*$/s, "");
585
+ return cleaned.trim();
586
+ }
587
+ /**
588
+ * Filter `<think>` blocks from a stream of token chunks.
589
+ *
590
+ * Accumulates text in *buffer* until we can determine whether content
591
+ * is inside a thinking block. Returns `[streamable, newBuffer, inside]`
592
+ * where *streamable* is the text safe to send to the client.
593
+ */
594
+ export function filterThinkingTokens(token, buffer, inside) {
595
+ buffer += token;
596
+ const streamableParts = [];
597
+ while (true) {
598
+ if (inside) {
599
+ const closeIdx = buffer.indexOf(THINK_CLOSE);
600
+ if (closeIdx === -1)
601
+ return [streamableParts.join(""), buffer, true];
602
+ buffer = buffer.slice(closeIdx + THINK_CLOSE.length);
603
+ inside = false;
604
+ }
605
+ else {
606
+ const openIdx = buffer.indexOf(THINK_OPEN);
607
+ if (openIdx === -1) {
608
+ // No complete `<think>` opener. The buffer may still end with a
609
+ // partial opener split across chunk boundaries (e.g. "…<thi"); flush
610
+ // everything before it but hold the partial back, otherwise the rest
611
+ // of the marker ("nk>secret…") would arrive next chunk and stream as
612
+ // plain text, leaking the hidden block.
613
+ const held = partialOpenSuffixLength(buffer);
614
+ const flushEnd = buffer.length - held;
615
+ streamableParts.push(buffer.slice(0, flushEnd));
616
+ return [streamableParts.join(""), buffer.slice(flushEnd), false];
617
+ }
618
+ streamableParts.push(buffer.slice(0, openIdx));
619
+ buffer = buffer.slice(openIdx + THINK_OPEN.length);
620
+ inside = true;
621
+ }
622
+ }
623
+ }
624
+ // =============================================================================
625
+ // Structured Logging Utilities
626
+ // =============================================================================
627
+ //
628
+ // `setupLogging` is intentionally NOT exported from this file (or from
629
+ // logger.ts) — full setup lives in `structured_logging.ts` alongside the
630
+ // agent-log JSON contract and stdout/stderr capture. Modules only need
631
+ // `getLogger(name)` for now.
632
+ // Truncation limits for log readability.
633
+ const MAX_CONTENT_LENGTH = 200;
634
+ const MAX_LOGGED_PAYLOAD_FIELDS = 20;
635
+ const MAX_LOGGED_FIELD_NAME_CHARS = 64;
636
+ const MAX_REDACT_FIELDS = 100;
637
+ function isLogUnsafeCodePoint(codePoint) {
638
+ // C0, DEL, and C1 (including CSI U+009B) must not reach the stdout log sink.
639
+ return codePoint < 32 || (codePoint >= 127 && codePoint <= 0x9f);
640
+ }
641
+ function boundedFieldName(key) {
642
+ const prefix = key.slice(0, MAX_LOGGED_FIELD_NAME_CHARS);
643
+ let safe = "";
644
+ for (const char of prefix) {
645
+ const codePoint = char.codePointAt(0) ?? 0;
646
+ safe += isLogUnsafeCodePoint(codePoint) ? "?" : char;
647
+ }
648
+ return safe + (key.length > MAX_LOGGED_FIELD_NAME_CHARS ? "..." : "");
649
+ }
650
+ function shallowValueSummary(value) {
651
+ if (typeof value === "string") {
652
+ return `string chars=${value.length}`;
653
+ }
654
+ if (value instanceof Uint8Array) {
655
+ return `bytes length=${value.byteLength}`;
656
+ }
657
+ if (Array.isArray(value)) {
658
+ return `array items=${value.length}`;
659
+ }
660
+ if (value instanceof Set) {
661
+ return `array items=${value.size}`;
662
+ }
663
+ if (value === null) {
664
+ return "null";
665
+ }
666
+ if (typeof value === "boolean") {
667
+ return "boolean";
668
+ }
669
+ if (typeof value === "number") {
670
+ return "number";
671
+ }
672
+ if (typeof value === "object") {
673
+ return "object";
674
+ }
675
+ return typeof value;
676
+ }
677
+ /** Describe a payload without serializing or recursively walking it. */
678
+ function payloadDebugSummary(payload, fieldsToRedact = []) {
679
+ if (payload === null ||
680
+ typeof payload !== "object" ||
681
+ Array.isArray(payload) ||
682
+ payload instanceof Uint8Array ||
683
+ payload instanceof Set ||
684
+ (Object.getPrototypeOf(payload) !== Object.prototype &&
685
+ Object.getPrototypeOf(payload) !== null)) {
686
+ return `type=${shallowValueSummary(payload)} values_omitted=true`;
687
+ }
688
+ const redactAll = fieldsToRedact.length > MAX_REDACT_FIELDS;
689
+ const redactedFields = redactAll
690
+ ? new Set()
691
+ : new Set(fieldsToRedact);
692
+ const fields = [];
693
+ let fieldsTruncated = false;
694
+ const record = payload;
695
+ for (const key in record) {
696
+ if (!Object.prototype.hasOwnProperty.call(record, key))
697
+ continue;
698
+ if (fields.length >= MAX_LOGGED_PAYLOAD_FIELDS) {
699
+ fieldsTruncated = true;
700
+ break;
701
+ }
702
+ const summary = redactAll || redactedFields.has(key)
703
+ ? "redacted"
704
+ : shallowValueSummary(record[key]);
705
+ fields.push(`${boundedFieldName(key)}=<${summary}>`);
706
+ }
707
+ return `fields=[${fields.join(", ")}] fields_truncated=${fieldsTruncated} values_omitted=true`;
708
+ }
709
+ /** Describe a result without exposing dynamic object keys. */
710
+ function resultDebugSummary(result) {
711
+ return `type=${shallowValueSummary(result)} values_omitted=true`;
712
+ }
713
+ /** Log a minor section separator (for individual operations). */
714
+ export function logSeparator() {
715
+ logger.info("-".repeat(40));
716
+ }
717
+ /** Log a major section separator (for execution boundaries). */
718
+ export function logSection() {
719
+ logger.info("=".repeat(60));
720
+ }
721
+ /**
722
+ * Log LLM conversation messages in a consistent format.
723
+ *
724
+ * @param messages List of message dicts or LangChain message objects
725
+ * @param prefix Log line prefix (e.g., "OE", "LLM", "AER")
726
+ * @param countOnly If true, only log message count (for info level)
727
+ */
728
+ export function logLLMMessages(messages, prefix = "LLM", countOnly = false) {
729
+ if (countOnly) {
730
+ logger.info(`${prefix}: ${messages.length} messages`);
731
+ return;
732
+ }
733
+ logger.debug(`${prefix}: ${messages.length} messages:`);
734
+ messages.forEach((msg, i) => {
735
+ const m = msg;
736
+ const msgType = m["type"] ?? "unknown";
737
+ const content = m["content"] ?? "";
738
+ const toolCalls = m["tool_calls"];
739
+ const preview = String(content).slice(0, MAX_CONTENT_LENGTH) || "(empty)";
740
+ if (toolCalls && Array.isArray(toolCalls)) {
741
+ logger.debug(`${prefix}: [${i}] ${msgType}: ${preview}... + ${toolCalls.length} tool_calls`);
742
+ }
743
+ else {
744
+ logger.debug(`${prefix}: [${i}] ${msgType}: ${preview}...`);
745
+ }
746
+ });
747
+ }
748
+ /**
749
+ * Log an LLM response in a consistent format.
750
+ *
751
+ * @param result LLM response (dict or AIMessage)
752
+ * @param step Step number
753
+ * @param prefix Log line prefix
754
+ */
755
+ export function logLLMResponse(result, step, prefix = "LLM") {
756
+ if (!result)
757
+ return;
758
+ const r = result;
759
+ const content = r["content"] ?? "";
760
+ const toolCalls = r["tool_calls"];
761
+ const preview = String(content).slice(0, MAX_CONTENT_LENGTH) || "(empty)";
762
+ logger.debug(`${prefix}: Step ${step} - Response: ${preview}...`);
763
+ if (toolCalls && Array.isArray(toolCalls)) {
764
+ const names = toolCalls.map((tc) => {
765
+ const t = tc;
766
+ return t["name"] ?? String(tc);
767
+ });
768
+ logger.info(`${prefix}: Step ${step} - Tool calls: ${names.join(", ")}`);
769
+ }
770
+ }
771
+ // Mirrors Python's agent_engine_runner_shared.logging.redact_fields.
772
+ /**
773
+ * Redact sensitive fields from an arguments record.
774
+ *
775
+ * Returns a copy with each listed field replaced by "[REDACTED]"; unlisted
776
+ * fields pass through unchanged, and an empty list returns the input as-is.
777
+ */
778
+ export function redactFields(data, fieldsToRedact) {
779
+ if (fieldsToRedact.length === 0)
780
+ return data;
781
+ const result = { ...data };
782
+ for (const field of fieldsToRedact) {
783
+ if (field in result) {
784
+ result[field] = "[REDACTED]";
785
+ }
786
+ }
787
+ return result;
788
+ }
789
+ /**
790
+ * Extract a tool's `redact_fields` policy from its registered definition.
791
+ * The definition is an opaque metadata record (framework-SDK populated), so
792
+ * non-string-array values degrade to no redaction rather than throwing.
793
+ */
794
+ export function toolRedactFields(toolDefinitions, toolName) {
795
+ const raw = toolName
796
+ ? toolDefinitions[toolName]?.["redact_fields"]
797
+ : undefined;
798
+ if (!Array.isArray(raw))
799
+ return [];
800
+ return raw.filter((f) => typeof f === "string");
801
+ }
802
+ /**
803
+ * Log a tool execution request.
804
+ *
805
+ * @param toolName Name of the tool
806
+ * @param args Tool arguments
807
+ * @param step Step number
808
+ * @param prefix Log line prefix
809
+ * @param fieldsToRedact Catalog sensitive-field policy. Matching fields omit
810
+ * even their type/length metadata; no values are logged.
811
+ */
812
+ export function logToolRequest(toolName, args, step, prefix = "TOOL", fieldsToRedact = []) {
813
+ logSeparator();
814
+ logger.info(`${prefix}: Step ${step} - ${toolName}`);
815
+ if (logger.isLevelEnabled("debug")) {
816
+ logger.debug(`${prefix}: Step ${step} - Arguments: ${payloadDebugSummary(args, fieldsToRedact)}`);
817
+ }
818
+ }
819
+ /**
820
+ * Log a tool execution result.
821
+ *
822
+ * @param toolName Name of the tool
823
+ * @param step Step number
824
+ * @param status Execution status (success, error, suspend, interrupted)
825
+ * @param result Tool result
826
+ * @param error Error message if failed
827
+ * @param durationMs Execution duration in milliseconds
828
+ * @param prefix Log line prefix
829
+ */
830
+ export function logToolResult(toolName, step, status, result = null, error = null, durationMs = 0, prefix = "TOOL") {
831
+ logger.info(`${prefix}: Step ${step} - ${toolName} ${status} (${durationMs.toFixed(0)}ms)`);
832
+ if (error) {
833
+ logger.error(`${prefix}: Step ${step} - Error: ${error}`);
834
+ }
835
+ else if (result !== null) {
836
+ if (logger.isLevelEnabled("debug")) {
837
+ logger.debug(`${prefix}: Step ${step} - Result: ${resultDebugSummary(result)}`);
838
+ }
839
+ }
840
+ }
841
+ /** Log that a cached result is being used (replay scenario). */
842
+ export function logCachedResult(toolName, step, prefix = "TOOL") {
843
+ logger.info(`${prefix}: Step ${step} - ${toolName} using CACHED result (replay)`);
844
+ }
845
+ /** Log that a tool call was blocked by policy. */
846
+ export function logPolicyBlocked(toolName, step, reason, prefix = "TOOL") {
847
+ logger.warn(`${prefix}: Step ${step} - ${toolName} BLOCKED by policy: ${reason}`);
848
+ }
849
+ /** Log the start of an execution. */
850
+ export function logExecutionStart(executionId, inputKeys, prefix = "OE") {
851
+ logSection();
852
+ logger.info(`${prefix}: Starting execution ${executionId.slice(0, 8)}...`);
853
+ logger.debug(`${prefix}: Input keys: ${inputKeys.join(", ")}`);
854
+ }
855
+ /** Log an execution callback (completion/suspension/error). */
856
+ export function logExecutionCallback(executionId, status, result = null, error = null, suspendReason = null, prefix = "OE") {
857
+ logSection();
858
+ logger.info(`${prefix}: Execution ${executionId.slice(0, 8)}... → ${status}`);
859
+ if (suspendReason)
860
+ logger.info(`${prefix}: Suspend reason: ${suspendReason}`);
861
+ if (error)
862
+ logger.error(`${prefix}: Error: ${error}`);
863
+ if (result) {
864
+ const preview = String(result).slice(0, 300);
865
+ logger.debug(`${prefix}: Result: ${preview}...`);
866
+ }
867
+ }