@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project database name resolution with historical-name compatibility.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the orchestration-engine Go implementation
|
|
5
|
+
* (`internal/domains/orchestration-engine/dbresolve.go`) and the Python copies
|
|
6
|
+
* (`mongomem_core/db_naming.py`, `agent_engine_runner_shared/db_naming.py`). The codebases
|
|
7
|
+
* are independent, so the algorithm is intentionally duplicated rather than
|
|
8
|
+
* shared. Keep them in sync. See
|
|
9
|
+
* `docs/decisions/008-per-project-database-isolation.md`.
|
|
10
|
+
*
|
|
11
|
+
* The runner/AER applies this to the agent *store* database only; memory
|
|
12
|
+
* writes go through the memory-server proxy, which scopes its own database.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Derive the effective database name for `base` scoped to `projectId`.
|
|
16
|
+
*
|
|
17
|
+
* 1. empty `projectId` -> return `base` unchanged.
|
|
18
|
+
* 2. `base` already ends with `_{projectId}` -> return as-is (idempotent).
|
|
19
|
+
* 3. `scoped = base + "_" + projectId`:
|
|
20
|
+
* - `scoped` exists on the cluster -> use it.
|
|
21
|
+
* - else an existing scoped `legacyBases` candidate -> use it.
|
|
22
|
+
* - else an existing unscoped `legacyBases` candidate -> use it.
|
|
23
|
+
* - else -> use `scoped` (fresh deployment).
|
|
24
|
+
*
|
|
25
|
+
* Unscoped fallback is limited to known platform defaults. The current base and arbitrary names are never auto-adopted.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveProjectScopedDb(base: string, projectId: string, existing: string[], legacyBases?: string[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether an empty PROJECT_ID must fail closed (REQUIRE_PROJECT_SCOPED_DB).
|
|
30
|
+
*
|
|
31
|
+
* ECP stamps this flag on managed AER pods (where PROJECT_ID is always injected),
|
|
32
|
+
* so an empty PROJECT_ID there fails closed rather than silently writing to the
|
|
33
|
+
* unscoped store. Local CLI dev leaves the flag unset and uses the unscoped name.
|
|
34
|
+
*/
|
|
35
|
+
export declare function projectScopingRequired(): boolean;
|
|
36
|
+
export interface ListsDatabaseNames {
|
|
37
|
+
listDatabaseNames(): Promise<string[]>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `base` against the live cluster via `listDatabaseNames`.
|
|
41
|
+
*
|
|
42
|
+
* Empty `projectId` throws when scoping is required (`required`, defaulting to
|
|
43
|
+
* {@link projectScopingRequired}), else warns and returns `base`. A listing
|
|
44
|
+
* failure throws so a transient error cannot create a competing current-name
|
|
45
|
+
* database beside an existing legacy one. `legacyBases` are previous defaults
|
|
46
|
+
* whose project-scoped forms, then bare forms, are adopted before a fresh
|
|
47
|
+
* scoped database is created.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveEffectiveDb(client: ListsDatabaseNames, base: string, projectId: string, opts?: {
|
|
50
|
+
required?: boolean;
|
|
51
|
+
label?: string;
|
|
52
|
+
legacyBases?: string[];
|
|
53
|
+
}): Promise<string>;
|
|
54
|
+
//# sourceMappingURL=db_naming.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db_naming.d.ts","sourceRoot":"","sources":["../src/db_naming.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAAE,EAClB,WAAW,GAAE,MAAM,EAAO,GACzB,MAAM,CAeR;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAIhD;AAED,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GACxE,OAAO,CAAC,MAAM,CAAC,CAuCjB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project database name resolution with historical-name compatibility.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the orchestration-engine Go implementation
|
|
5
|
+
* (`internal/domains/orchestration-engine/dbresolve.go`) and the Python copies
|
|
6
|
+
* (`mongomem_core/db_naming.py`, `agent_engine_runner_shared/db_naming.py`). The codebases
|
|
7
|
+
* are independent, so the algorithm is intentionally duplicated rather than
|
|
8
|
+
* shared. Keep them in sync. See
|
|
9
|
+
* `docs/decisions/008-per-project-database-isolation.md`.
|
|
10
|
+
*
|
|
11
|
+
* The runner/AER applies this to the agent *store* database only; memory
|
|
12
|
+
* writes go through the memory-server proxy, which scopes its own database.
|
|
13
|
+
*/
|
|
14
|
+
import { getLogger } from "./logger.js";
|
|
15
|
+
const logger = getLogger("agent_engine_runner_shared.db_naming");
|
|
16
|
+
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
17
|
+
/**
|
|
18
|
+
* Derive the effective database name for `base` scoped to `projectId`.
|
|
19
|
+
*
|
|
20
|
+
* 1. empty `projectId` -> return `base` unchanged.
|
|
21
|
+
* 2. `base` already ends with `_{projectId}` -> return as-is (idempotent).
|
|
22
|
+
* 3. `scoped = base + "_" + projectId`:
|
|
23
|
+
* - `scoped` exists on the cluster -> use it.
|
|
24
|
+
* - else an existing scoped `legacyBases` candidate -> use it.
|
|
25
|
+
* - else an existing unscoped `legacyBases` candidate -> use it.
|
|
26
|
+
* - else -> use `scoped` (fresh deployment).
|
|
27
|
+
*
|
|
28
|
+
* Unscoped fallback is limited to known platform defaults. The current base and arbitrary names are never auto-adopted.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveProjectScopedDb(base, projectId, existing, legacyBases = []) {
|
|
31
|
+
if (!projectId)
|
|
32
|
+
return base;
|
|
33
|
+
const suffix = "_" + projectId;
|
|
34
|
+
if (base.endsWith(suffix))
|
|
35
|
+
return base;
|
|
36
|
+
const scoped = base + suffix;
|
|
37
|
+
const names = new Set(existing);
|
|
38
|
+
if (names.has(scoped))
|
|
39
|
+
return scoped;
|
|
40
|
+
for (const legacy of legacyBases) {
|
|
41
|
+
const legacyScoped = legacy + suffix;
|
|
42
|
+
if (names.has(legacyScoped))
|
|
43
|
+
return legacyScoped;
|
|
44
|
+
}
|
|
45
|
+
for (const legacy of legacyBases) {
|
|
46
|
+
if (names.has(legacy))
|
|
47
|
+
return legacy;
|
|
48
|
+
}
|
|
49
|
+
return scoped;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether an empty PROJECT_ID must fail closed (REQUIRE_PROJECT_SCOPED_DB).
|
|
53
|
+
*
|
|
54
|
+
* ECP stamps this flag on managed AER pods (where PROJECT_ID is always injected),
|
|
55
|
+
* so an empty PROJECT_ID there fails closed rather than silently writing to the
|
|
56
|
+
* unscoped store. Local CLI dev leaves the flag unset and uses the unscoped name.
|
|
57
|
+
*/
|
|
58
|
+
export function projectScopingRequired() {
|
|
59
|
+
return TRUTHY.has((process.env["REQUIRE_PROJECT_SCOPED_DB"] ?? "").trim().toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve `base` against the live cluster via `listDatabaseNames`.
|
|
63
|
+
*
|
|
64
|
+
* Empty `projectId` throws when scoping is required (`required`, defaulting to
|
|
65
|
+
* {@link projectScopingRequired}), else warns and returns `base`. A listing
|
|
66
|
+
* failure throws so a transient error cannot create a competing current-name
|
|
67
|
+
* database beside an existing legacy one. `legacyBases` are previous defaults
|
|
68
|
+
* whose project-scoped forms, then bare forms, are adopted before a fresh
|
|
69
|
+
* scoped database is created.
|
|
70
|
+
*/
|
|
71
|
+
export async function resolveEffectiveDb(client, base, projectId, opts = {}) {
|
|
72
|
+
const required = opts.required ?? projectScopingRequired();
|
|
73
|
+
const label = opts.label ?? "database";
|
|
74
|
+
if (!projectId) {
|
|
75
|
+
if (required) {
|
|
76
|
+
throw new Error(`PROJECT_ID is empty but per-project DB isolation is required ` +
|
|
77
|
+
`(REQUIRE_PROJECT_SCOPED_DB); refusing to use the unscoped ${label} '${base}'`);
|
|
78
|
+
}
|
|
79
|
+
logger.warn(`PROJECT_ID is empty; per-project DB isolation disabled, using unscoped ${label} '${base}'`);
|
|
80
|
+
return base;
|
|
81
|
+
}
|
|
82
|
+
let existing;
|
|
83
|
+
try {
|
|
84
|
+
existing = await client.listDatabaseNames();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new Error(`database discovery failed for project '${projectId}'; refusing to select a database`, { cause: err });
|
|
88
|
+
}
|
|
89
|
+
const resolved = resolveProjectScopedDb(base, projectId, existing, opts.legacyBases);
|
|
90
|
+
if (resolved !== base + "_" + projectId && !base.endsWith("_" + projectId)) {
|
|
91
|
+
logger.info(`using historical ${label} '${resolved}' for project '${projectId}'`);
|
|
92
|
+
}
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared local error reporting helpers for CLI-generated runtime flows.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `agent_engine_runner_shared/error_reporting.py`. Reports agent runtime/build
|
|
5
|
+
* failures to Sentry, gated by env (`AGENTIC_SENTRY_ENABLED=1` + a DSN). All
|
|
6
|
+
* outbound payloads are scrubbed for secrets and the caller's home directory
|
|
7
|
+
* via `beforeSend` plus per-value redaction before capture.
|
|
8
|
+
*
|
|
9
|
+
* `@sentry/node` is an *optional* dependency (a devDependency here for types
|
|
10
|
+
* and tests): it is loaded via a guarded dynamic import only when reporting is
|
|
11
|
+
* enabled, so agents that never flip `AGENTIC_SENTRY_ENABLED` never install or
|
|
12
|
+
* pay for it. When the module isn't present, or anything in the reporting path
|
|
13
|
+
* faults, every function degrades to a no-op — reporting must never block an
|
|
14
|
+
* agent from starting or mask its real failure.
|
|
15
|
+
*/
|
|
16
|
+
type SeverityLevel = "fatal" | "error" | "warning" | "log" | "info" | "debug";
|
|
17
|
+
/** @internal — test-only override of the resolved home directory. */
|
|
18
|
+
export declare function setHomeDirForTest(dir: string): void;
|
|
19
|
+
export interface InitErrorReportingArgs {
|
|
20
|
+
surface: string;
|
|
21
|
+
component?: string | null;
|
|
22
|
+
mode?: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize Sentry error reporting. No-op (returns `false`) unless
|
|
26
|
+
* `AGENTIC_SENTRY_ENABLED=1` and `AGENTIC_SENTRY_DSN` are both set, and
|
|
27
|
+
* `@sentry/node` is installed. Tags every event with `surface` and, when
|
|
28
|
+
* provided, `component` / `mode`.
|
|
29
|
+
*
|
|
30
|
+
* Never throws: a missing module or a faulting `Sentry.init` leaves reporting
|
|
31
|
+
* disabled rather than propagating — enabling observability must not turn into
|
|
32
|
+
* a boot blocker.
|
|
33
|
+
*/
|
|
34
|
+
export declare function initErrorReporting(args: InitErrorReportingArgs): Promise<boolean>;
|
|
35
|
+
export interface CaptureExceptionArgs {
|
|
36
|
+
summary?: string | null;
|
|
37
|
+
extra?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Capture an exception. When `summary` is given, the reported error message is
|
|
41
|
+
* the (redacted) summary with the original exception's type/message preserved
|
|
42
|
+
* as scoped extras — matching Python's `_exception_for_capture`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function captureException(exc: unknown, args?: CaptureExceptionArgs): void;
|
|
45
|
+
export declare function captureMessage(message: string, level?: SeverityLevel, extra?: Record<string, unknown>): void;
|
|
46
|
+
export declare function flush(timeoutMs?: number): Promise<void>;
|
|
47
|
+
export interface SubprocessFailure {
|
|
48
|
+
/** The command that failed — array (argv) or a single string. */
|
|
49
|
+
command: string | readonly string[];
|
|
50
|
+
/** Process exit code, if known. */
|
|
51
|
+
exitCode?: number | null;
|
|
52
|
+
stdout?: string | Uint8Array | null;
|
|
53
|
+
stderr?: string | Uint8Array | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A one-line summary of a failed subprocess: the command label plus the most
|
|
57
|
+
* meaningful line of its output, or the exit code when no output stands out.
|
|
58
|
+
*/
|
|
59
|
+
export declare function summarizeSubprocessFailure(exc: SubprocessFailure): string;
|
|
60
|
+
/** The trailing `MAX_OUTPUT_TAIL_CHARS` of a subprocess's stdout+stderr. */
|
|
61
|
+
export declare function subprocessOutputTail(exc: SubprocessFailure): string;
|
|
62
|
+
/** @internal — exported only for tests; not part of the public API. */
|
|
63
|
+
export declare function beforeSend(event: Record<string, unknown>, _hint?: unknown): Record<string, unknown>;
|
|
64
|
+
/** @internal — exported only for tests; not part of the public API. */
|
|
65
|
+
export declare function redactText(text: string): string;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=error_reporting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error_reporting.d.ts","sourceRoot":"","sources":["../src/error_reporting.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,KAAK,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAiC9E,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAwClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,OAAO,EACZ,IAAI,GAAE,oBAAyB,GAC9B,IAAI,CAwBN;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,aAAuB,EAC9B,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAClC,IAAI,CAaN;AAED,wBAAsB,KAAK,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3D;AAMD,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACpC,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAKzE;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAUnE;AAMD,uEAAuE;AACvE,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,CAAC,EAAE,OAAO,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwBzB;AAmCD,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS/C"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared local error reporting helpers for CLI-generated runtime flows.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `agent_engine_runner_shared/error_reporting.py`. Reports agent runtime/build
|
|
5
|
+
* failures to Sentry, gated by env (`AGENTIC_SENTRY_ENABLED=1` + a DSN). All
|
|
6
|
+
* outbound payloads are scrubbed for secrets and the caller's home directory
|
|
7
|
+
* via `beforeSend` plus per-value redaction before capture.
|
|
8
|
+
*
|
|
9
|
+
* `@sentry/node` is an *optional* dependency (a devDependency here for types
|
|
10
|
+
* and tests): it is loaded via a guarded dynamic import only when reporting is
|
|
11
|
+
* enabled, so agents that never flip `AGENTIC_SENTRY_ENABLED` never install or
|
|
12
|
+
* pay for it. When the module isn't present, or anything in the reporting path
|
|
13
|
+
* faults, every function degrades to a no-op — reporting must never block an
|
|
14
|
+
* agent from starting or mask its real failure.
|
|
15
|
+
*/
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
// The lazily-imported @sentry/node module; null until a successful enable.
|
|
18
|
+
let sentry = null;
|
|
19
|
+
let enabled = false;
|
|
20
|
+
const MAX_SUMMARY_LINE_LENGTH = 240;
|
|
21
|
+
const MAX_OUTPUT_TAIL_CHARS = 8 * 1024;
|
|
22
|
+
// Cap redaction recursion so a self-referential or pathologically nested
|
|
23
|
+
// caller-supplied `extra` can't drive `redactValue` into a stack overflow.
|
|
24
|
+
const MAX_REDACT_DEPTH = 12;
|
|
25
|
+
// `key=value` / `key: value` inline secrets. Case-insensitive; keeps the key
|
|
26
|
+
// and separator, redacts the value up to the next whitespace/comma/semicolon.
|
|
27
|
+
const SECRET_INLINE_PATTERN = /\b(api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)(=|:)\s*([^\s,;]+)/gi;
|
|
28
|
+
const BEARER_PATTERN = /\b(bearer)\s+[A-Za-z0-9._-]+/gi;
|
|
29
|
+
const URL_USERINFO_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)[^:/\s@]+:[^@\s/]+@/gi;
|
|
30
|
+
// Bare-token form (scheme://token@host, no ":" separator) — e.g. a GitHub PAT
|
|
31
|
+
// embedded as `https://ghp_xxx@github.com`. Applied after URL_USERINFO_PATTERN
|
|
32
|
+
// so a user:pass@ pair is never double-matched by this looser pattern.
|
|
33
|
+
const URL_USERINFO_BARE_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)[^:/\s@]+@/gi;
|
|
34
|
+
function resolveHomeDir() {
|
|
35
|
+
try {
|
|
36
|
+
return homedir().trim();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return (process.env["HOME"] ?? "").trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let homeDir = resolveHomeDir();
|
|
43
|
+
/** @internal — test-only override of the resolved home directory. */
|
|
44
|
+
export function setHomeDirForTest(dir) {
|
|
45
|
+
homeDir = dir;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Initialize Sentry error reporting. No-op (returns `false`) unless
|
|
49
|
+
* `AGENTIC_SENTRY_ENABLED=1` and `AGENTIC_SENTRY_DSN` are both set, and
|
|
50
|
+
* `@sentry/node` is installed. Tags every event with `surface` and, when
|
|
51
|
+
* provided, `component` / `mode`.
|
|
52
|
+
*
|
|
53
|
+
* Never throws: a missing module or a faulting `Sentry.init` leaves reporting
|
|
54
|
+
* disabled rather than propagating — enabling observability must not turn into
|
|
55
|
+
* a boot blocker.
|
|
56
|
+
*/
|
|
57
|
+
export async function initErrorReporting(args) {
|
|
58
|
+
enabled = false;
|
|
59
|
+
sentry = null;
|
|
60
|
+
const dsn = (process.env["AGENTIC_SENTRY_DSN"] ?? "").trim();
|
|
61
|
+
if (!(process.env["AGENTIC_SENTRY_ENABLED"] === "1" && dsn))
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
sentry = await import("@sentry/node");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// @sentry/node not installed — reporting is opt-in, so stay disabled.
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
sentry.init({
|
|
72
|
+
dsn,
|
|
73
|
+
environment: process.env["AGENTIC_SENTRY_ENVIRONMENT"] ?? "local-dev",
|
|
74
|
+
release: process.env["AGENTIC_SENTRY_RELEASE"] || undefined,
|
|
75
|
+
tracesSampleRate: 0.0,
|
|
76
|
+
debug: process.env["AGENTIC_SENTRY_DEBUG"] === "1",
|
|
77
|
+
// Our scrubber operates on a generic event dict and returns the same
|
|
78
|
+
// (mutated) object; narrow the return to the callback's event type.
|
|
79
|
+
beforeSend: (event, hint) => beforeSend(event, hint),
|
|
80
|
+
});
|
|
81
|
+
sentry.setTag("surface", args.surface);
|
|
82
|
+
if (args.component)
|
|
83
|
+
sentry.setTag("component", args.component);
|
|
84
|
+
if (args.mode)
|
|
85
|
+
sentry.setTag("mode", args.mode);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// A malformed DSN/environment or any init fault disables reporting rather
|
|
89
|
+
// than crashing the agent at boot.
|
|
90
|
+
sentry = null;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
enabled = true;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Capture an exception. When `summary` is given, the reported error message is
|
|
98
|
+
* the (redacted) summary with the original exception's type/message preserved
|
|
99
|
+
* as scoped extras — matching Python's `_exception_for_capture`.
|
|
100
|
+
*/
|
|
101
|
+
export function captureException(exc, args = {}) {
|
|
102
|
+
if (!enabled || !sentry)
|
|
103
|
+
return;
|
|
104
|
+
const s = sentry;
|
|
105
|
+
// Reporting must never throw into the caller — a fault here would mask the
|
|
106
|
+
// very failure the caller is trying to report.
|
|
107
|
+
try {
|
|
108
|
+
const { summary, extra } = args;
|
|
109
|
+
const captured = exceptionForCapture(exc, summary);
|
|
110
|
+
s.withScope((scope) => {
|
|
111
|
+
if (summary) {
|
|
112
|
+
scope.setExtra("original_exception_type", exc instanceof Error ? exc.name : typeof exc);
|
|
113
|
+
scope.setExtra("original_exception_message", redactText(String(exc)));
|
|
114
|
+
}
|
|
115
|
+
for (const [key, value] of Object.entries(extra ?? {})) {
|
|
116
|
+
scope.setExtra(key, redactValue(value));
|
|
117
|
+
}
|
|
118
|
+
s.captureException(captured);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* swallow — best-effort reporting */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function captureMessage(message, level = "error", extra = {}) {
|
|
126
|
+
if (!enabled || !sentry)
|
|
127
|
+
return;
|
|
128
|
+
const s = sentry;
|
|
129
|
+
try {
|
|
130
|
+
s.withScope((scope) => {
|
|
131
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
132
|
+
scope.setExtra(key, redactValue(value));
|
|
133
|
+
}
|
|
134
|
+
s.captureMessage(redactText(message), level);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* swallow — best-effort reporting */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function flush(timeoutMs = 2000) {
|
|
142
|
+
if (!enabled || !sentry)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
await sentry.flush(timeoutMs);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* swallow — best-effort reporting */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A one-line summary of a failed subprocess: the command label plus the most
|
|
153
|
+
* meaningful line of its output, or the exit code when no output stands out.
|
|
154
|
+
*/
|
|
155
|
+
export function summarizeSubprocessFailure(exc) {
|
|
156
|
+
const command = commandLabel(exc.command);
|
|
157
|
+
const headline = failureHeadline(subprocessOutputTail(exc));
|
|
158
|
+
if (headline)
|
|
159
|
+
return `${command} failed: ${headline}`;
|
|
160
|
+
return `${command} failed with exit ${exc.exitCode ?? "unknown"}`;
|
|
161
|
+
}
|
|
162
|
+
/** The trailing `MAX_OUTPUT_TAIL_CHARS` of a subprocess's stdout+stderr. */
|
|
163
|
+
export function subprocessOutputTail(exc) {
|
|
164
|
+
const chunks = [];
|
|
165
|
+
for (const value of [exc.stdout, exc.stderr]) {
|
|
166
|
+
if (value === null || value === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
chunks.push(typeof value === "string" ? value : Buffer.from(value).toString("utf-8"));
|
|
169
|
+
}
|
|
170
|
+
if (chunks.length === 0)
|
|
171
|
+
return "";
|
|
172
|
+
return tailText(chunks.join("\n"));
|
|
173
|
+
}
|
|
174
|
+
// =============================================================================
|
|
175
|
+
// Redaction — internal, but pure and unit-tested directly
|
|
176
|
+
// =============================================================================
|
|
177
|
+
/** @internal — exported only for tests; not part of the public API. */
|
|
178
|
+
export function beforeSend(event, _hint) {
|
|
179
|
+
const data = event;
|
|
180
|
+
data["server_name"] = null;
|
|
181
|
+
if ("message" in data) {
|
|
182
|
+
data["message"] = redactText(String(data["message"]));
|
|
183
|
+
}
|
|
184
|
+
for (const section of [
|
|
185
|
+
"request",
|
|
186
|
+
"user",
|
|
187
|
+
"tags",
|
|
188
|
+
"extra",
|
|
189
|
+
"contexts",
|
|
190
|
+
"exception",
|
|
191
|
+
"breadcrumbs",
|
|
192
|
+
"threads",
|
|
193
|
+
"logentry",
|
|
194
|
+
"modules",
|
|
195
|
+
]) {
|
|
196
|
+
const payload = data[section];
|
|
197
|
+
if (payload !== null && typeof payload === "object") {
|
|
198
|
+
data[section] = redactValue(payload);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return event;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Recursively redact strings in a value. Guards against caller-supplied
|
|
205
|
+
* `extra` that is self-referential (WeakSet cycle check) or pathologically
|
|
206
|
+
* deep (`MAX_REDACT_DEPTH`): either would otherwise overflow the stack and
|
|
207
|
+
* take down the reporting path. A cycle/over-depth node is replaced with a
|
|
208
|
+
* sentinel rather than recursed into.
|
|
209
|
+
*/
|
|
210
|
+
function redactValue(value, depth = 0, seen = new WeakSet()) {
|
|
211
|
+
if (typeof value === "string")
|
|
212
|
+
return redactText(value);
|
|
213
|
+
if (value === null || typeof value !== "object")
|
|
214
|
+
return value;
|
|
215
|
+
if (seen.has(value))
|
|
216
|
+
return "[Circular]";
|
|
217
|
+
if (depth >= MAX_REDACT_DEPTH)
|
|
218
|
+
return "[MaxDepth]";
|
|
219
|
+
seen.add(value);
|
|
220
|
+
const result = Array.isArray(value)
|
|
221
|
+
? value.map((item) => redactValue(item, depth + 1, seen))
|
|
222
|
+
: Object.fromEntries(Object.entries(value).map(([k, v]) => [
|
|
223
|
+
k,
|
|
224
|
+
redactValue(v, depth + 1, seen),
|
|
225
|
+
]));
|
|
226
|
+
// Allow the same object reached again via a sibling branch (a DAG, not a
|
|
227
|
+
// cycle) to serialize in full: only nodes on the current path are guarded.
|
|
228
|
+
seen.delete(value);
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
/** @internal — exported only for tests; not part of the public API. */
|
|
232
|
+
export function redactText(text) {
|
|
233
|
+
if (!text)
|
|
234
|
+
return text;
|
|
235
|
+
let redacted = text;
|
|
236
|
+
if (homeDir)
|
|
237
|
+
redacted = redacted.split(homeDir).join("<home>");
|
|
238
|
+
redacted = redacted.replace(URL_USERINFO_PATTERN, "$1<redacted>:<redacted>@");
|
|
239
|
+
redacted = redacted.replace(URL_USERINFO_BARE_PATTERN, "$1<redacted>@");
|
|
240
|
+
redacted = redacted.replace(SECRET_INLINE_PATTERN, "$1$2<redacted>");
|
|
241
|
+
redacted = redacted.replace(BEARER_PATTERN, "$1 <redacted>");
|
|
242
|
+
return redacted;
|
|
243
|
+
}
|
|
244
|
+
function exceptionForCapture(exc, summary) {
|
|
245
|
+
if (!summary)
|
|
246
|
+
return exc;
|
|
247
|
+
const wrapped = new Error(redactText(summary));
|
|
248
|
+
// Preserve the original stack so Sentry groups on the real failure site.
|
|
249
|
+
if (exc instanceof Error && exc.stack)
|
|
250
|
+
wrapped.stack = exc.stack;
|
|
251
|
+
return wrapped;
|
|
252
|
+
}
|
|
253
|
+
function commandLabel(cmd) {
|
|
254
|
+
if (Array.isArray(cmd)) {
|
|
255
|
+
const parts = cmd.map(String).filter((p) => p.trim());
|
|
256
|
+
if (parts.length === 0)
|
|
257
|
+
return "subprocess";
|
|
258
|
+
if (parts.length >= 3 && parts[0] === "uv" && parts[1] === "pip") {
|
|
259
|
+
return parts.slice(0, 3).join(" ");
|
|
260
|
+
}
|
|
261
|
+
if (parts.length >= 2)
|
|
262
|
+
return parts.slice(0, 2).join(" ");
|
|
263
|
+
return parts[0];
|
|
264
|
+
}
|
|
265
|
+
const text = String(cmd).trim();
|
|
266
|
+
return text || "subprocess";
|
|
267
|
+
}
|
|
268
|
+
function failureHeadline(output) {
|
|
269
|
+
const normalized = output
|
|
270
|
+
.split("\n")
|
|
271
|
+
.map(normalizeFailureLine)
|
|
272
|
+
.filter((line) => line);
|
|
273
|
+
for (let i = normalized.length - 1; i >= 0; i--) {
|
|
274
|
+
const line = normalized[i];
|
|
275
|
+
const lower = line.toLowerCase();
|
|
276
|
+
if (lower.includes("failed") ||
|
|
277
|
+
lower.includes("error") ||
|
|
278
|
+
lower.includes("not found") ||
|
|
279
|
+
lower.includes("no matching") ||
|
|
280
|
+
lower.includes("exception")) {
|
|
281
|
+
return truncateSummaryLine(line);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (normalized.length === 0)
|
|
285
|
+
return "";
|
|
286
|
+
return truncateSummaryLine(normalized[normalized.length - 1]);
|
|
287
|
+
}
|
|
288
|
+
function normalizeFailureLine(line) {
|
|
289
|
+
let cleaned = line
|
|
290
|
+
.trim()
|
|
291
|
+
.replace(/^[│>]+/, "")
|
|
292
|
+
.trim();
|
|
293
|
+
if (cleaned.startsWith("×"))
|
|
294
|
+
cleaned = cleaned.slice(1).trim();
|
|
295
|
+
if (cleaned.startsWith("╰─▶"))
|
|
296
|
+
cleaned = cleaned.slice(3).trim();
|
|
297
|
+
if (cleaned.toLowerCase().startsWith("error:"))
|
|
298
|
+
cleaned = cleaned.slice(6).trim();
|
|
299
|
+
return cleaned;
|
|
300
|
+
}
|
|
301
|
+
function tailText(text) {
|
|
302
|
+
const trimmed = text.trim();
|
|
303
|
+
if (trimmed.length <= MAX_OUTPUT_TAIL_CHARS)
|
|
304
|
+
return trimmed;
|
|
305
|
+
return trimmed.slice(-MAX_OUTPUT_TAIL_CHARS);
|
|
306
|
+
}
|
|
307
|
+
function truncateSummaryLine(line) {
|
|
308
|
+
if (line.length <= MAX_SUMMARY_LINE_LENGTH)
|
|
309
|
+
return line;
|
|
310
|
+
return line.slice(0, MAX_SUMMARY_LINE_LENGTH - 3) + "...";
|
|
311
|
+
}
|