@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE.md +201 -0
- package/README.md +29 -0
- package/dist/agent_config.d.ts +167 -0
- package/dist/agent_config.d.ts.map +1 -0
- package/dist/agent_config.js +544 -0
- package/dist/call_interrupted.d.ts +12 -0
- package/dist/call_interrupted.d.ts.map +1 -0
- package/dist/call_interrupted.js +11 -0
- package/dist/checkpoint_workspace.d.ts +25 -0
- package/dist/checkpoint_workspace.d.ts.map +1 -0
- package/dist/checkpoint_workspace.js +44 -0
- package/dist/context.d.ts +235 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +322 -0
- package/dist/db_config.d.ts +28 -0
- package/dist/db_config.d.ts.map +1 -0
- package/dist/db_config.js +66 -0
- package/dist/db_naming.d.ts +54 -0
- package/dist/db_naming.d.ts.map +1 -0
- package/dist/db_naming.js +94 -0
- package/dist/error_reporting.d.ts +67 -0
- package/dist/error_reporting.d.ts.map +1 -0
- package/dist/error_reporting.js +311 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/activity_pb.js +115 -0
- package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
- package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/common_pb.js +86 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/runtime_pb.js +40 -0
- package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
- package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/state_pb.js +68 -0
- package/dist/guardrails_evaluator/core.d.ts +23 -0
- package/dist/guardrails_evaluator/core.d.ts.map +1 -0
- package/dist/guardrails_evaluator/core.js +122 -0
- package/dist/guardrails_evaluator/index.d.ts +10 -0
- package/dist/guardrails_evaluator/index.d.ts.map +1 -0
- package/dist/guardrails_evaluator/index.js +11 -0
- package/dist/guardrails_evaluator/regex.d.ts +20 -0
- package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
- package/dist/guardrails_evaluator/regex.js +233 -0
- package/dist/hooks.d.ts +109 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +216 -0
- package/dist/http_path.d.ts +18 -0
- package/dist/http_path.d.ts.map +1 -0
- package/dist/http_path.js +53 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/launcher.d.ts +130 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +325 -0
- package/dist/logger.d.ts +96 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +204 -0
- package/dist/mcp_oauth.d.ts +51 -0
- package/dist/mcp_oauth.d.ts.map +1 -0
- package/dist/mcp_oauth.js +389 -0
- package/dist/mcp_oauth_secret.d.ts +21 -0
- package/dist/mcp_oauth_secret.d.ts.map +1 -0
- package/dist/mcp_oauth_secret.js +122 -0
- package/dist/mcp_tools.d.ts +71 -0
- package/dist/mcp_tools.d.ts.map +1 -0
- package/dist/mcp_tools.js +301 -0
- package/dist/memory_appbound.d.ts +42 -0
- package/dist/memory_appbound.d.ts.map +1 -0
- package/dist/memory_appbound.js +159 -0
- package/dist/memory_writer.d.ts +49 -0
- package/dist/memory_writer.d.ts.map +1 -0
- package/dist/memory_writer.js +171 -0
- package/dist/metrics.d.ts +84 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +205 -0
- package/dist/models.d.ts +1458 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +1726 -0
- package/dist/node_logger.d.ts +43 -0
- package/dist/node_logger.d.ts.map +1 -0
- package/dist/node_logger.js +158 -0
- package/dist/owner_callback.d.ts +16 -0
- package/dist/owner_callback.d.ts.map +1 -0
- package/dist/owner_callback.js +40 -0
- package/dist/progress.d.ts +57 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +140 -0
- package/dist/runtime.d.ts +131 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +351 -0
- package/dist/secure_llm_proxy.d.ts +115 -0
- package/dist/secure_llm_proxy.d.ts.map +1 -0
- package/dist/secure_llm_proxy.js +922 -0
- package/dist/secure_wrapper.d.ts +332 -0
- package/dist/secure_wrapper.d.ts.map +1 -0
- package/dist/secure_wrapper.js +1249 -0
- package/dist/server/aer.d.ts +61 -0
- package/dist/server/aer.d.ts.map +1 -0
- package/dist/server/aer.js +1124 -0
- package/dist/server/auth.d.ts +56 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +132 -0
- package/dist/server/base.d.ts +104 -0
- package/dist/server/base.d.ts.map +1 -0
- package/dist/server/base.js +150 -0
- package/dist/server/callInterrupt.d.ts +49 -0
- package/dist/server/callInterrupt.d.ts.map +1 -0
- package/dist/server/callInterrupt.js +68 -0
- package/dist/server/callback_delivery.d.ts +14 -0
- package/dist/server/callback_delivery.d.ts.map +1 -0
- package/dist/server/callback_delivery.js +141 -0
- package/dist/server/chunk_types.d.ts +50 -0
- package/dist/server/chunk_types.d.ts.map +1 -0
- package/dist/server/chunk_types.js +62 -0
- package/dist/server/cors.d.ts +52 -0
- package/dist/server/cors.d.ts.map +1 -0
- package/dist/server/cors.js +107 -0
- package/dist/server/drain.d.ts +169 -0
- package/dist/server/drain.d.ts.map +1 -0
- package/dist/server/drain.js +455 -0
- package/dist/server/function.d.ts +77 -0
- package/dist/server/function.d.ts.map +1 -0
- package/dist/server/function.js +337 -0
- package/dist/server/http_retry.d.ts +37 -0
- package/dist/server/http_retry.d.ts.map +1 -0
- package/dist/server/http_retry.js +157 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/metadata.d.ts +50 -0
- package/dist/server/metadata.d.ts.map +1 -0
- package/dist/server/metadata.js +193 -0
- package/dist/server/oe_url.d.ts +36 -0
- package/dist/server/oe_url.d.ts.map +1 -0
- package/dist/server/oe_url.js +50 -0
- package/dist/server/owner_url.d.ts +35 -0
- package/dist/server/owner_url.d.ts.map +1 -0
- package/dist/server/owner_url.js +146 -0
- package/dist/server/query.d.ts +42 -0
- package/dist/server/query.d.ts.map +1 -0
- package/dist/server/query.js +28 -0
- package/dist/server/tool.d.ts +138 -0
- package/dist/server/tool.d.ts.map +1 -0
- package/dist/server/tool.js +1017 -0
- package/dist/span_names.d.ts +21 -0
- package/dist/span_names.d.ts.map +1 -0
- package/dist/span_names.js +31 -0
- package/dist/structured_logging/constants.d.ts +17 -0
- package/dist/structured_logging/constants.d.ts.map +1 -0
- package/dist/structured_logging/constants.js +71 -0
- package/dist/structured_logging/env.d.ts +18 -0
- package/dist/structured_logging/env.d.ts.map +1 -0
- package/dist/structured_logging/env.js +39 -0
- package/dist/structured_logging/install.d.ts +56 -0
- package/dist/structured_logging/install.d.ts.map +1 -0
- package/dist/structured_logging/install.js +107 -0
- package/dist/structured_logging/layout.d.ts +9 -0
- package/dist/structured_logging/layout.d.ts.map +1 -0
- package/dist/structured_logging/layout.js +144 -0
- package/dist/structured_logging/serialize.d.ts +27 -0
- package/dist/structured_logging/serialize.d.ts.map +1 -0
- package/dist/structured_logging/serialize.js +61 -0
- package/dist/structured_logging/stdio_capture.d.ts +59 -0
- package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
- package/dist/structured_logging/stdio_capture.js +164 -0
- package/dist/structured_logging/uncaught.d.ts +14 -0
- package/dist/structured_logging/uncaught.d.ts.map +1 -0
- package/dist/structured_logging/uncaught.js +58 -0
- package/dist/structured_logging.d.ts +48 -0
- package/dist/structured_logging.d.ts.map +1 -0
- package/dist/structured_logging.js +47 -0
- package/dist/tls_client.d.ts +61 -0
- package/dist/tls_client.d.ts.map +1 -0
- package/dist/tls_client.js +298 -0
- package/dist/tool_api_error.d.ts +62 -0
- package/dist/tool_api_error.d.ts.map +1 -0
- package/dist/tool_api_error.js +399 -0
- package/dist/tool_memory_ownership.d.ts +10 -0
- package/dist/tool_memory_ownership.d.ts.map +1 -0
- package/dist/tool_memory_ownership.js +36 -0
- package/dist/toolpod_handlers.d.ts +126 -0
- package/dist/toolpod_handlers.d.ts.map +1 -0
- package/dist/toolpod_handlers.js +1016 -0
- package/dist/tracing/exporters.d.ts +51 -0
- package/dist/tracing/exporters.d.ts.map +1 -0
- package/dist/tracing/exporters.js +327 -0
- package/dist/tracing/index.d.ts +3 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +2 -0
- package/dist/tracing/setup.d.ts +76 -0
- package/dist/tracing/setup.d.ts.map +1 -0
- package/dist/tracing/setup.js +436 -0
- package/dist/utils.d.ts +204 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +867 -0
- package/dist/workflow/activity.d.ts +71 -0
- package/dist/workflow/activity.d.ts.map +1 -0
- package/dist/workflow/activity.js +357 -0
- package/dist/workflow/attempt.d.ts +12 -0
- package/dist/workflow/attempt.d.ts.map +1 -0
- package/dist/workflow/attempt.js +96 -0
- package/dist/workflow/client.d.ts +46 -0
- package/dist/workflow/client.d.ts.map +1 -0
- package/dist/workflow/client.js +299 -0
- package/dist/workflow/context.d.ts +37 -0
- package/dist/workflow/context.d.ts.map +1 -0
- package/dist/workflow/context.js +350 -0
- package/dist/workflow/heartbeat.d.ts +15 -0
- package/dist/workflow/heartbeat.d.ts.map +1 -0
- package/dist/workflow/heartbeat.js +78 -0
- package/dist/workflow/index.d.ts +14 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/memory.d.ts +17 -0
- package/dist/workflow/memory.d.ts.map +1 -0
- package/dist/workflow/memory.js +184 -0
- 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
|
+
}
|