pi-langfuse 1.5.6 → 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 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.6",
3
+ "version": "1.5.7",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
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
 
@@ -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), 20000) : undefined,
82
+ systemPrompt: systemPrompt ? truncate(String(systemPrompt), getLimits().maxString) : undefined,
83
83
  },
84
84
  getCapturePolicy(),
85
85
  );
@@ -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: MAX_TOOL_PAYLOAD_LENGTH });
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, MAX_TOOL_PAYLOAD_LENGTH);
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, MAX_TOOL_PAYLOAD_LENGTH) ??
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: MAX_TOOL_PAYLOAD_LENGTH });
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, MAX_TOOL_PAYLOAD_LENGTH);
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/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
- const DEFAULT_OPTIONS: RedactOptions = {
13
- maxDepth: 6,
14
- maxArrayItems: 50,
15
- maxObjectKeys: 80,
16
- maxStringLength: 12_000,
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 = { ...DEFAULT_OPTIONS, ...options };
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 = { ...DEFAULT_OPTIONS, ...options };
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 function truncate(value: string, maxLength = MAX_STRING_LENGTH): string {
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: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean; parseJson?: boolean } = {},
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 maxString = options.maxString ?? MAX_STRING_LENGTH;
41
- const depth = options.depth ?? MAX_DEPTH;
42
- const maxNodes = options.maxNodes ?? MAX_PAYLOAD_NODES;
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, MAX_ARRAY_ITEMS);
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 >= MAX_OBJECT_KEYS) {
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: MAX_ARRAY_ITEMS,
140
- maxObjectKeys: MAX_OBJECT_KEYS,
145
+ maxArrayItems,
146
+ maxObjectKeys,
141
147
  });
142
148
  }
143
149
 
144
- export function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGTH): string {
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 = MAX_TOOL_PAYLOAD_LENGTH): number {
158
+ export function estimatePayloadBytes(value: unknown, maxLength = getLimits().maxToolPayload): number {
153
159
  return new TextEncoder().encode(safeSerialize(value, maxLength)).length;
154
160
  }
155
161