pi-langfuse 1.5.5 → 1.5.7
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/README.md +21 -0
- package/package.json +3 -1
- package/src/config.ts +3 -0
- package/src/handlers/agent.ts +2 -2
- package/src/handlers/tool.ts +6 -6
- package/src/langfuse.ts +41 -1
- package/src/limits.ts +97 -0
- package/src/redaction.ts +12 -8
- package/src/types.ts +2 -0
- package/src/utils.ts +25 -19
package/README.md
CHANGED
|
@@ -104,6 +104,27 @@ export LANGFUSE_CAPTURE_CWD=false
|
|
|
104
104
|
|
|
105
105
|
All captured payloads are redacted before upload. The extension masks common API keys, bearer tokens, passwords, cookies, private keys, Langfuse keys, GitHub/npm/AWS-style tokens, and local absolute paths.
|
|
106
106
|
|
|
107
|
+
### Payload limits
|
|
108
|
+
|
|
109
|
+
Before upload, payloads are shaped: strings are truncated and deeply nested or
|
|
110
|
+
very wide structures are trimmed. These caps keep traces small and protect the
|
|
111
|
+
Langfuse ingestion pipeline. Override any of them (no rebuild needed):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
export PI_LANGFUSE_MAX_STRING_LENGTH=12000 # per-string chars (system prompt, inputs)
|
|
115
|
+
export PI_LANGFUSE_MAX_TOOL_PAYLOAD_LENGTH=24000 # per tool input/output chars
|
|
116
|
+
export PI_LANGFUSE_MAX_DEPTH=6 # max nesting depth
|
|
117
|
+
export PI_LANGFUSE_MAX_ARRAY_ITEMS=50 # max array elements kept
|
|
118
|
+
export PI_LANGFUSE_MAX_OBJECT_KEYS=80 # max object keys kept
|
|
119
|
+
export PI_LANGFUSE_MAX_PAYLOAD_NODES=2000 # max total nodes per payload
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Set any limit to `0`, `off`, `none`, or `unlimited` to disable that cap
|
|
123
|
+
entirely (captures the full value). Unset or invalid values fall back to the
|
|
124
|
+
defaults shown above. To capture a very large system prompt or big tool
|
|
125
|
+
payloads in full, raise or disable the relevant limit (e.g.
|
|
126
|
+
`PI_LANGFUSE_MAX_STRING_LENGTH=off`).
|
|
127
|
+
|
|
107
128
|
### Method 3: Persistent `config.json`
|
|
108
129
|
|
|
109
130
|
Create or update `~/.pi/agent/pi-langfuse/config.json`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"@langfuse/client": "^5.3.0",
|
|
47
47
|
"@langfuse/otel": "^5.3.0",
|
|
48
48
|
"@langfuse/tracing": "^5.3.0",
|
|
49
|
+
"@opentelemetry/api": "^1.9.0",
|
|
50
|
+
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
49
51
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
50
52
|
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
|
51
53
|
},
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
|
5
5
|
import { state } from "./state.js";
|
|
6
6
|
import { forceShutdownRuntime } from "./langfuse.js";
|
|
7
7
|
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
8
|
+
import { createPayloadLimits } from "./limits.js";
|
|
8
9
|
|
|
9
10
|
export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
|
|
10
11
|
if (existsSync(path)) {
|
|
@@ -22,6 +23,7 @@ export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.en
|
|
|
22
23
|
secretKey: config.secretKey,
|
|
23
24
|
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
24
25
|
capturePolicy: createCapturePolicy(captureSource),
|
|
26
|
+
limits: createPayloadLimits(env),
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
} catch (e) {
|
|
@@ -44,6 +46,7 @@ export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config
|
|
|
44
46
|
secretKey,
|
|
45
47
|
host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
46
48
|
capturePolicy: createCapturePolicy(env),
|
|
49
|
+
limits: createPayloadLimits(env),
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
package/src/handlers/agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { state, resetRunState, computeEvaluationScores } from "../state.js";
|
|
2
2
|
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
3
|
import { ensureConfig } from "../config.js";
|
|
4
|
-
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
|
|
4
|
+
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy, getLimits } from "../utils.js";
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
6
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
7
|
import { collectSourceMetadata } from "../source-metadata.js";
|
|
@@ -79,7 +79,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
79
79
|
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
80
80
|
sessionId: state.currentSessionId || undefined,
|
|
81
81
|
},
|
|
82
|
-
systemPrompt: systemPrompt ? truncate(String(systemPrompt),
|
|
82
|
+
systemPrompt: systemPrompt ? truncate(String(systemPrompt), getLimits().maxString) : undefined,
|
|
83
83
|
},
|
|
84
84
|
getCapturePolicy(),
|
|
85
85
|
);
|
package/src/handlers/tool.ts
CHANGED
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
truncate,
|
|
11
11
|
estimatePayloadBytes,
|
|
12
12
|
getCapturePolicy,
|
|
13
|
+
getLimits,
|
|
13
14
|
} from "../utils.js";
|
|
14
|
-
import { MAX_TOOL_PAYLOAD_LENGTH } from "../constants.js";
|
|
15
15
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
16
16
|
import { redactString } from "../redaction.js";
|
|
17
17
|
|
|
@@ -28,7 +28,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
28
28
|
try {
|
|
29
29
|
const toolName = getToolName(event);
|
|
30
30
|
const toolInput = getToolInput(event);
|
|
31
|
-
const shapedInput = shapePayload(toolInput, { maxString:
|
|
31
|
+
const shapedInput = shapePayload(toolInput, { maxString: getLimits().maxToolPayload });
|
|
32
32
|
const captured = applyCapturePolicy(
|
|
33
33
|
{
|
|
34
34
|
toolInput: shapedInput,
|
|
@@ -36,7 +36,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
36
36
|
},
|
|
37
37
|
getCapturePolicy(),
|
|
38
38
|
);
|
|
39
|
-
const inputBytes = estimatePayloadBytes(captured.toolInput,
|
|
39
|
+
const inputBytes = estimatePayloadBytes(captured.toolInput, getLimits().maxToolPayload);
|
|
40
40
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
41
41
|
const tool = await startChildObservation({
|
|
42
42
|
parent,
|
|
@@ -79,7 +79,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
79
79
|
|
|
80
80
|
const isError = Boolean(event.isError ?? event.error ?? event.status === "error");
|
|
81
81
|
const output =
|
|
82
|
-
extractTextContent(event.content,
|
|
82
|
+
extractTextContent(event.content, getLimits().maxToolPayload) ??
|
|
83
83
|
event.output ??
|
|
84
84
|
event.result ??
|
|
85
85
|
event.error ??
|
|
@@ -87,7 +87,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
87
87
|
event;
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
|
-
const shapedOutput = shapePayload(output, { maxString:
|
|
90
|
+
const shapedOutput = shapePayload(output, { maxString: getLimits().maxToolPayload });
|
|
91
91
|
const captured = applyCapturePolicy(
|
|
92
92
|
{
|
|
93
93
|
toolOutput: shapedOutput,
|
|
@@ -99,7 +99,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
99
99
|
},
|
|
100
100
|
getCapturePolicy(),
|
|
101
101
|
);
|
|
102
|
-
const outputBytes = estimatePayloadBytes(captured.toolOutput,
|
|
102
|
+
const outputBytes = estimatePayloadBytes(captured.toolOutput, getLimits().maxToolPayload);
|
|
103
103
|
const durationMs = Math.max(0, Date.now() - activeTool.startedAt);
|
|
104
104
|
|
|
105
105
|
activeTool.observation
|
package/src/langfuse.ts
CHANGED
|
@@ -3,11 +3,23 @@ import { state } from "./state.js";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
5
5
|
let runtime: LangfuseRuntime | null = null;
|
|
6
|
+
let registeredContextManager: OtelContextManager | null = null;
|
|
6
7
|
const activeSessions = new Set<string>();
|
|
7
8
|
let lastRuntimeError: { scope: string; message: string; timestamp: Date } | null = null;
|
|
8
9
|
|
|
9
10
|
type FallbackObservationType = "SPAN" | "GENERATION";
|
|
10
11
|
|
|
12
|
+
interface OtelContextManager {
|
|
13
|
+
enable(): OtelContextManager;
|
|
14
|
+
disable(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OtelContextApi {
|
|
18
|
+
setGlobalContextManager(contextManager: OtelContextManager): boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type AsyncHooksContextManagerCtor = new () => OtelContextManager;
|
|
22
|
+
|
|
11
23
|
interface RestFallbackTrace {
|
|
12
24
|
id: string;
|
|
13
25
|
timestamp: string;
|
|
@@ -65,6 +77,24 @@ function debugLog(message: string) {
|
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
export function ensureOtelContextManager(
|
|
81
|
+
contextApi: OtelContextApi,
|
|
82
|
+
AsyncHooksContextManager: AsyncHooksContextManagerCtor,
|
|
83
|
+
): boolean {
|
|
84
|
+
if (registeredContextManager) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const contextManager = new AsyncHooksContextManager().enable();
|
|
89
|
+
if (contextApi.setGlobalContextManager(contextManager)) {
|
|
90
|
+
registeredContextManager = contextManager;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
contextManager.disable();
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
function rememberRuntimeError(scope: string, error: unknown) {
|
|
69
99
|
lastRuntimeError = {
|
|
70
100
|
scope,
|
|
@@ -372,8 +402,17 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
372
402
|
}
|
|
373
403
|
|
|
374
404
|
if (!runtime) {
|
|
375
|
-
const [
|
|
405
|
+
const [
|
|
406
|
+
{ BasicTracerProvider },
|
|
407
|
+
{ context },
|
|
408
|
+
{ AsyncHooksContextManager },
|
|
409
|
+
{ LangfuseSpanProcessor },
|
|
410
|
+
tracing,
|
|
411
|
+
{ LangfuseClient },
|
|
412
|
+
] = await Promise.all([
|
|
376
413
|
import("@opentelemetry/sdk-trace-base"),
|
|
414
|
+
import("@opentelemetry/api"),
|
|
415
|
+
import("@opentelemetry/context-async-hooks"),
|
|
377
416
|
import("@langfuse/otel"),
|
|
378
417
|
import("@langfuse/tracing"),
|
|
379
418
|
import("@langfuse/client"),
|
|
@@ -386,6 +425,7 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
386
425
|
};
|
|
387
426
|
|
|
388
427
|
try {
|
|
428
|
+
ensureOtelContextManager(context, AsyncHooksContextManager);
|
|
389
429
|
const spanProcessor = new LangfuseSpanProcessor({
|
|
390
430
|
publicKey: state.config.publicKey,
|
|
391
431
|
secretKey: state.config.secretKey,
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_ARRAY_ITEMS,
|
|
3
|
+
MAX_DEPTH,
|
|
4
|
+
MAX_OBJECT_KEYS,
|
|
5
|
+
MAX_PAYLOAD_NODES,
|
|
6
|
+
MAX_STRING_LENGTH,
|
|
7
|
+
MAX_TOOL_PAYLOAD_LENGTH,
|
|
8
|
+
} from "./constants.js";
|
|
9
|
+
import type { EnvLike } from "./capture-policy.js";
|
|
10
|
+
import { state } from "./state.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Payload-shaping limits. Every field is a positive integer, or
|
|
14
|
+
* `Number.POSITIVE_INFINITY` to disable that limit entirely (capture everything).
|
|
15
|
+
* Resolved once from the environment and stored on the loaded config; consumers
|
|
16
|
+
* read the resolved values via `getLimits()` rather than the raw constants.
|
|
17
|
+
*/
|
|
18
|
+
export interface PayloadLimits {
|
|
19
|
+
/** Max characters kept per captured string (generation/agent inputs, outputs, system prompt). */
|
|
20
|
+
readonly maxString: number;
|
|
21
|
+
/** Max characters kept for tool inputs/outputs (their payloads run larger than chat strings). */
|
|
22
|
+
readonly maxToolPayload: number;
|
|
23
|
+
/** Max nesting depth walked when shaping a structured payload. */
|
|
24
|
+
readonly maxDepth: number;
|
|
25
|
+
/** Max array elements kept per array. */
|
|
26
|
+
readonly maxArrayItems: number;
|
|
27
|
+
/** Max own-keys kept per object. */
|
|
28
|
+
readonly maxObjectKeys: number;
|
|
29
|
+
/** Max total nodes visited across a whole payload before bailing with `[payload too large]`. */
|
|
30
|
+
readonly maxNodes: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_LIMITS: PayloadLimits = {
|
|
34
|
+
maxString: MAX_STRING_LENGTH,
|
|
35
|
+
maxToolPayload: MAX_TOOL_PAYLOAD_LENGTH,
|
|
36
|
+
maxDepth: MAX_DEPTH,
|
|
37
|
+
maxArrayItems: MAX_ARRAY_ITEMS,
|
|
38
|
+
maxObjectKeys: MAX_OBJECT_KEYS,
|
|
39
|
+
maxNodes: MAX_PAYLOAD_NODES,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Words that mean "no limit" when supplied as an env value. */
|
|
43
|
+
const UNLIMITED_WORDS = new Set(["off", "none", "false", "no", "unlimited", "inf", "infinity"]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse one limit env value.
|
|
47
|
+
* - unset / blank / unparseable -> `fallback` (the built-in default)
|
|
48
|
+
* - "off"/"none"/"unlimited"/... or a value <= 0 -> `Infinity` (limit removed)
|
|
49
|
+
* - a positive number -> that integer
|
|
50
|
+
*/
|
|
51
|
+
export function parseLimit(raw: string | undefined, fallback: number): number {
|
|
52
|
+
if (raw === undefined) {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
const trimmed = raw.trim().toLowerCase();
|
|
56
|
+
if (trimmed === "") {
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
if (UNLIMITED_WORDS.has(trimmed)) {
|
|
60
|
+
return Number.POSITIVE_INFINITY;
|
|
61
|
+
}
|
|
62
|
+
const parsed = Number(trimmed);
|
|
63
|
+
if (!Number.isFinite(parsed)) {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
if (parsed <= 0) {
|
|
67
|
+
return Number.POSITIVE_INFINITY;
|
|
68
|
+
}
|
|
69
|
+
return Math.floor(parsed);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve payload limits from the environment. Each `PI_LANGFUSE_MAX_*` var overrides
|
|
74
|
+
* the corresponding default; set any to `0`/`off`/`unlimited` to remove that limit.
|
|
75
|
+
* Namespaced `PI_LANGFUSE_*` (not `LANGFUSE_*`) to avoid clashing with Langfuse
|
|
76
|
+
* server env vars such as `LANGFUSE_MAX_EVENT_SIZE_BYTES`.
|
|
77
|
+
*/
|
|
78
|
+
export function createPayloadLimits(env: EnvLike = process.env as EnvLike): PayloadLimits {
|
|
79
|
+
return {
|
|
80
|
+
maxString: parseLimit(env.PI_LANGFUSE_MAX_STRING_LENGTH, DEFAULT_LIMITS.maxString),
|
|
81
|
+
maxToolPayload: parseLimit(env.PI_LANGFUSE_MAX_TOOL_PAYLOAD_LENGTH, DEFAULT_LIMITS.maxToolPayload),
|
|
82
|
+
maxDepth: parseLimit(env.PI_LANGFUSE_MAX_DEPTH, DEFAULT_LIMITS.maxDepth),
|
|
83
|
+
maxArrayItems: parseLimit(env.PI_LANGFUSE_MAX_ARRAY_ITEMS, DEFAULT_LIMITS.maxArrayItems),
|
|
84
|
+
maxObjectKeys: parseLimit(env.PI_LANGFUSE_MAX_OBJECT_KEYS, DEFAULT_LIMITS.maxObjectKeys),
|
|
85
|
+
maxNodes: parseLimit(env.PI_LANGFUSE_MAX_PAYLOAD_NODES, DEFAULT_LIMITS.maxNodes),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolved limits for the current session: the config-loaded values when a
|
|
91
|
+
* config is active, otherwise a fresh resolve from the environment. Every
|
|
92
|
+
* capture/redaction path reads limits through this so a single env change
|
|
93
|
+
* (or config) governs truncation everywhere.
|
|
94
|
+
*/
|
|
95
|
+
export function getLimits(): PayloadLimits {
|
|
96
|
+
return state.config?.limits ?? createPayloadLimits();
|
|
97
|
+
}
|
package/src/redaction.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { getLimits } from "./limits.js";
|
|
2
3
|
|
|
3
4
|
export const REDACTED = "[REDACTED_SECRET]";
|
|
4
5
|
|
|
@@ -9,12 +10,15 @@ export interface RedactOptions {
|
|
|
9
10
|
maxStringLength: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
function defaultOptions(): RedactOptions {
|
|
14
|
+
const limits = getLimits();
|
|
15
|
+
return {
|
|
16
|
+
maxDepth: limits.maxDepth,
|
|
17
|
+
maxArrayItems: limits.maxArrayItems,
|
|
18
|
+
maxObjectKeys: limits.maxObjectKeys,
|
|
19
|
+
maxStringLength: limits.maxString,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
const SECRET_ASSIGNMENT_RE =
|
|
20
24
|
/\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASS|API[_-]?KEY|PRIVATE[_-]?KEY|AUTH|COOKIE)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
|
|
@@ -36,7 +40,7 @@ function truncate(value: string, maxStringLength: number): string {
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export function redactString(value: string, options: Partial<RedactOptions> = {}): string {
|
|
39
|
-
const merged = { ...
|
|
43
|
+
const merged = { ...defaultOptions(), ...options };
|
|
40
44
|
const truncated = truncate(value, merged.maxStringLength);
|
|
41
45
|
return truncated
|
|
42
46
|
.replace(PRIVATE_KEY_RE, REDACTED)
|
|
@@ -110,6 +114,6 @@ function visit(value: unknown, options: RedactOptions, depth: number, seen: Weak
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
export function redactValue(value: unknown, options: Partial<RedactOptions> = {}): unknown {
|
|
113
|
-
const merged: RedactOptions = { ...
|
|
117
|
+
const merged: RedactOptions = { ...defaultOptions(), ...options };
|
|
114
118
|
return visit(value, merged, merged.maxDepth, new WeakSet<object>());
|
|
115
119
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { CapturePolicy } from "./capture-policy.js";
|
|
2
|
+
import type { PayloadLimits } from "./limits.js";
|
|
2
3
|
|
|
3
4
|
export interface Config {
|
|
4
5
|
publicKey: string;
|
|
5
6
|
secretKey: string;
|
|
6
7
|
host: string;
|
|
7
8
|
capturePolicy?: CapturePolicy;
|
|
9
|
+
limits?: PayloadLimits;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export interface LangfuseObservation {
|
package/src/utils.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_ARRAY_ITEMS,
|
|
3
|
-
MAX_DEPTH,
|
|
4
|
-
MAX_OBJECT_KEYS,
|
|
5
|
-
MAX_PAYLOAD_NODES,
|
|
6
|
-
MAX_STRING_LENGTH,
|
|
7
|
-
MAX_TOOL_PAYLOAD_LENGTH,
|
|
8
|
-
} from "./constants.js";
|
|
1
|
+
import { getLimits } from "./limits.js";
|
|
9
2
|
import { createCapturePolicy, type CapturePolicy } from "./capture-policy.js";
|
|
10
3
|
import { redactValue } from "./redaction.js";
|
|
11
4
|
import { state } from "./state.js";
|
|
@@ -14,7 +7,9 @@ export function getCapturePolicy(): CapturePolicy {
|
|
|
14
7
|
return state.config?.capturePolicy ?? createCapturePolicy();
|
|
15
8
|
}
|
|
16
9
|
|
|
17
|
-
export
|
|
10
|
+
export { getLimits };
|
|
11
|
+
|
|
12
|
+
export function truncate(value: string, maxLength = getLimits().maxString): string {
|
|
18
13
|
return value.length > maxLength ? `${value.slice(0, maxLength)}... [truncated]` : value;
|
|
19
14
|
}
|
|
20
15
|
|
|
@@ -35,11 +30,22 @@ const PAYLOAD_TOO_LARGE = "[payload too large]";
|
|
|
35
30
|
|
|
36
31
|
export function shapePayload(
|
|
37
32
|
value: unknown,
|
|
38
|
-
options: {
|
|
33
|
+
options: {
|
|
34
|
+
maxString?: number;
|
|
35
|
+
depth?: number;
|
|
36
|
+
maxNodes?: number;
|
|
37
|
+
maxArrayItems?: number;
|
|
38
|
+
maxObjectKeys?: number;
|
|
39
|
+
redact?: boolean;
|
|
40
|
+
parseJson?: boolean;
|
|
41
|
+
} = {},
|
|
39
42
|
): unknown {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
+
const limits = getLimits();
|
|
44
|
+
const maxString = options.maxString ?? limits.maxString;
|
|
45
|
+
const depth = options.depth ?? limits.maxDepth;
|
|
46
|
+
const maxNodes = options.maxNodes ?? limits.maxNodes;
|
|
47
|
+
const maxArrayItems = options.maxArrayItems ?? limits.maxArrayItems;
|
|
48
|
+
const maxObjectKeys = options.maxObjectKeys ?? limits.maxObjectKeys;
|
|
43
49
|
const budget = { exhausted: false, nodeCount: 0 };
|
|
44
50
|
|
|
45
51
|
function visit(item: unknown, remainingDepth: number, seen: WeakSet<object>): unknown {
|
|
@@ -88,7 +94,7 @@ export function shapePayload(
|
|
|
88
94
|
|
|
89
95
|
if (Array.isArray(item)) {
|
|
90
96
|
const output: unknown[] = [];
|
|
91
|
-
const limit = Math.min(item.length,
|
|
97
|
+
const limit = Math.min(item.length, maxArrayItems);
|
|
92
98
|
for (let index = 0; index < limit; index++) {
|
|
93
99
|
output.push(visit(item[index], remainingDepth - 1, seen));
|
|
94
100
|
if (budget.exhausted) {
|
|
@@ -120,7 +126,7 @@ export function shapePayload(
|
|
|
120
126
|
}
|
|
121
127
|
output[key] = visit((item as Record<string, unknown>)[key], remainingDepth - 1, seen);
|
|
122
128
|
keyCount++;
|
|
123
|
-
if (budget.exhausted || keyCount >=
|
|
129
|
+
if (budget.exhausted || keyCount >= maxObjectKeys) {
|
|
124
130
|
break;
|
|
125
131
|
}
|
|
126
132
|
}
|
|
@@ -136,12 +142,12 @@ export function shapePayload(
|
|
|
136
142
|
: redactValue(shaped, {
|
|
137
143
|
maxDepth: depth,
|
|
138
144
|
maxStringLength: maxString,
|
|
139
|
-
maxArrayItems
|
|
140
|
-
maxObjectKeys
|
|
145
|
+
maxArrayItems,
|
|
146
|
+
maxObjectKeys,
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
export function safeSerialize(value: unknown, maxLength =
|
|
150
|
+
export function safeSerialize(value: unknown, maxLength = getLimits().maxToolPayload): string {
|
|
145
151
|
try {
|
|
146
152
|
return truncate(JSON.stringify(shapePayload(value, { maxString: maxLength }), null, 2), maxLength);
|
|
147
153
|
} catch {
|
|
@@ -149,7 +155,7 @@ export function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGT
|
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
export function estimatePayloadBytes(value: unknown, maxLength =
|
|
158
|
+
export function estimatePayloadBytes(value: unknown, maxLength = getLimits().maxToolPayload): number {
|
|
153
159
|
return new TextEncoder().encode(safeSerialize(value, maxLength)).length;
|
|
154
160
|
}
|
|
155
161
|
|