pi-langfuse 1.4.1 → 1.4.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/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import { basename } from "node:path";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
 
13
- import { state, resetRunState } from "./src/state.js";
13
+ import { state, resetRunState, runWithSession, setCurrentSession } from "./src/state.js";
14
14
  import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
15
15
  import { shutdownRuntime } from "./src/langfuse.js";
16
16
  import { getMessageFromEvent, extractAssistantOutput } from "./src/utils.js";
@@ -51,72 +51,79 @@ export default async function (pi: ExtensionAPI) {
51
51
  },
52
52
  });
53
53
 
54
- pi.on("session_start", async (_event, ctx) => {
54
+ const getSessionId = (ctx?: any) => {
55
+ try {
56
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
57
+ return sessionFile ? basename(sessionFile, ".jsonl") : undefined;
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ };
62
+
63
+ const withSession = <T>(ctx: any, fn: () => T): T => runWithSession(getSessionId(ctx) ?? state.currentSessionId, fn);
64
+
65
+ pi.on("session_start", async (_event, ctx) => withSession(ctx, async () => {
55
66
  state.setupAttemptedThisSession = false;
56
67
  await ensureConfig(ctx);
57
- const sessionFile = ctx.sessionManager.getSessionFile();
58
- if (sessionFile) {
59
- state.currentSessionId = basename(sessionFile, ".jsonl");
60
- }
61
68
  resetRunState();
62
- });
69
+ }));
63
70
 
64
- pi.on("model_select", async (event) => {
71
+ pi.on("model_select", async (event, ctx) => withSession(ctx, async () => {
65
72
  state.currentModel = event.model?.id || "";
66
73
  state.currentProvider = event.model?.provider || "";
67
- });
74
+ }));
68
75
 
69
- pi.on("before_agent_start", async (event, ctx) => {
76
+ pi.on("before_agent_start", async (event, ctx) => withSession(ctx, async () => {
70
77
  await startAgentRun(event, ctx);
71
- });
78
+ }));
72
79
 
73
- pi.on("agent_start", async (event, ctx) => {
80
+ pi.on("agent_start", async (event, ctx) => withSession(ctx, async () => {
74
81
  if (!state.agentState?.root) {
75
82
  await startAgentRun(event, ctx);
76
83
  }
77
- });
84
+ }));
78
85
 
79
- pi.on("turn_start", async (event) => {
86
+ pi.on("turn_start", async (event, ctx) => withSession(ctx, async () => {
80
87
  await startTurnObservation(event);
81
- });
88
+ }));
82
89
 
83
- pi.on("before_provider_request", async (event) => {
90
+ pi.on("before_provider_request", async (event, ctx) => withSession(ctx, async () => {
84
91
  await startGeneration(event);
85
- });
92
+ }));
86
93
 
87
- pi.on("after_provider_response", async (event) => {
94
+ pi.on("after_provider_response", async (event, ctx) => withSession(ctx, async () => {
88
95
  updateGenerationMetadata(event);
89
- });
96
+ }));
90
97
 
91
- pi.on("message_update", async (event) => {
98
+ pi.on("message_update", async (event, ctx) => withSession(ctx, async () => {
92
99
  recordTTFT(event);
93
100
  const message = getMessageFromEvent(event);
94
101
  if (message?.role === "assistant" && state.agentState) {
95
102
  state.agentState.latestAssistantOutput = extractAssistantOutput(message);
96
103
  }
97
- });
104
+ }));
98
105
 
99
- pi.on("message_end", async (event) => {
106
+ pi.on("message_end", async (event, ctx) => withSession(ctx, async () => {
100
107
  await finishGenerationFromMessage(event);
101
- });
108
+ }));
102
109
 
103
- pi.on("tool_execution_start", async (event) => {
110
+ pi.on("tool_execution_start", async (event, ctx) => withSession(ctx, async () => {
104
111
  await startToolObservation(event);
105
- });
112
+ }));
106
113
 
107
- pi.on("tool_call", async (event) => {
114
+ pi.on("tool_call", async (event, ctx) => withSession(ctx, async () => {
108
115
  await startToolObservation(event);
109
- });
116
+ }));
110
117
 
111
- pi.on("tool_result", async (event) => {
118
+ pi.on("tool_result", async (event, ctx) => withSession(ctx, async () => {
112
119
  await finishToolObservation(event);
113
- });
120
+ }));
114
121
 
115
- pi.on("tool_execution_end", async (event) => {
122
+ pi.on("tool_execution_end", async (event, ctx) => withSession(ctx, async () => {
116
123
  await finishToolObservation(event);
117
- });
124
+ }));
118
125
 
119
- pi.on("turn_end", async (event) => {
126
+ pi.on("turn_end", async (event, ctx) => withSession(ctx, async () => {
120
127
  state.turnCount++;
121
128
  const message = getMessageFromEvent(event);
122
129
  if (message?.role === "assistant") {
@@ -124,16 +131,16 @@ export default async function (pi: ExtensionAPI) {
124
131
  await finishGenerationFromMessage(event);
125
132
  }
126
133
  finishTurnObservation(event);
127
- });
134
+ }));
128
135
 
129
- pi.on("agent_end", async (event) => {
136
+ pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
130
137
  await finishAgentRun(event);
131
138
  setTimeout(() => {
132
139
  shutdownRuntime().catch((error) => {
133
140
  console.warn("📊 Langfuse: Deferred shutdown failed", error);
134
141
  });
135
142
  }, 0);
136
- });
143
+ }));
137
144
 
138
145
  const handleSessionInterruption = (reason: string) => {
139
146
  if (state.agentState?.root) {
@@ -143,15 +150,21 @@ export default async function (pi: ExtensionAPI) {
143
150
  resetRunState();
144
151
  };
145
152
 
146
- pi.on("session_before_switch", async () => {
147
- handleSessionInterruption("Session switched");
153
+ pi.on("session_before_switch", async (_event, ctx) => {
154
+ const sessionId = getSessionId(ctx);
155
+ if (sessionId) {
156
+ setCurrentSession(sessionId);
157
+ }
148
158
  });
149
159
 
150
- pi.on("session_before_fork", async () => {
151
- handleSessionInterruption("Session forked");
160
+ pi.on("session_before_fork", async (_event, ctx) => {
161
+ const sessionId = getSessionId(ctx);
162
+ if (sessionId) {
163
+ setCurrentSession(sessionId);
164
+ }
152
165
  });
153
166
 
154
- pi.on("session_compact", async (event) => {
167
+ pi.on("session_compact", async (event, ctx) => withSession(ctx, async () => {
155
168
  if (state.agentState?.root) {
156
169
  const parent = state.agentState.activeTurn ?? state.agentState.root;
157
170
  try {
@@ -169,10 +182,10 @@ export default async function (pi: ExtensionAPI) {
169
182
  // ignore
170
183
  }
171
184
  }
172
- });
185
+ }));
173
186
 
174
- pi.on("session_shutdown", async () => {
187
+ pi.on("session_shutdown", async (_event, ctx) => withSession(ctx, async () => {
175
188
  handleSessionInterruption("Session shutdown before agent completed");
176
189
  await shutdownRuntime();
177
- });
190
+ }));
178
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,16 @@
12
12
  "homepage": "https://github.com/gooyoung/pi-langfuse#readme",
13
13
  "type": "module",
14
14
  "main": "index.ts",
15
+ "files": [
16
+ "index.ts",
17
+ "src/",
18
+ "types/",
19
+ "README.md",
20
+ "README_CN.md",
21
+ "image.png",
22
+ "skills-lock.json",
23
+ "tsconfig.json"
24
+ ],
15
25
  "scripts": {
16
26
  "typecheck": "tsc --noEmit"
17
27
  },
package/src/constants.ts CHANGED
@@ -10,3 +10,4 @@ export const MAX_TOOL_PAYLOAD_LENGTH = 24_000;
10
10
  export const MAX_DEPTH = 6;
11
11
  export const MAX_ARRAY_ITEMS = 50;
12
12
  export const MAX_OBJECT_KEYS = 80;
13
+ export const MAX_PAYLOAD_NODES = 2_000;
@@ -19,6 +19,7 @@ export function updateTraceIO(input?: unknown, output?: unknown) {
19
19
 
20
20
  export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
21
21
  if (!(await ensureConfig(ctx))) {
22
+ state.isTracingDisabled = true;
22
23
  return;
23
24
  }
24
25
 
@@ -92,6 +93,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
92
93
  updateTraceIO(promptInput, undefined);
93
94
  } catch (e) {
94
95
  console.warn("📊 Langfuse: Failed to create agent observation", e);
96
+ state.isTracingDisabled = true;
95
97
  }
96
98
  }
97
99
 
@@ -13,7 +13,7 @@ import {
13
13
  import type { GenerationState, ObservationUpdate } from "../types.js";
14
14
 
15
15
  export function getOpenGeneration(): GenerationState | undefined {
16
- if (!state.agentState) {
16
+ if (state.isTracingDisabled || !state.agentState) {
17
17
  return undefined;
18
18
  }
19
19
 
@@ -29,7 +29,7 @@ export function getOpenGeneration(): GenerationState | undefined {
29
29
  }
30
30
 
31
31
  export async function startGeneration(event: Record<string, unknown>) {
32
- if (!state.agentState?.root) {
32
+ if (state.isTracingDisabled || !state.agentState?.root) {
33
33
  return;
34
34
  }
35
35
 
@@ -79,7 +79,7 @@ export async function startGeneration(event: Record<string, unknown>) {
79
79
  }
80
80
 
81
81
  export function updateGenerationMetadata(event: Record<string, unknown>) {
82
- if (!state.agentState) {
82
+ if (state.isTracingDisabled || !state.agentState) {
83
83
  return;
84
84
  }
85
85
 
@@ -132,7 +132,7 @@ export function updateGenerationMetadata(event: Record<string, unknown>) {
132
132
  }
133
133
 
134
134
  export function recordTTFT(event: Record<string, unknown>) {
135
- if (!state.agentState) {
135
+ if (state.isTracingDisabled || !state.agentState) {
136
136
  return;
137
137
  }
138
138
 
@@ -150,7 +150,7 @@ export function recordTTFT(event: Record<string, unknown>) {
150
150
  }
151
151
 
152
152
  export async function finishGenerationFromMessage(event: Record<string, unknown>) {
153
- if (!state.agentState) {
153
+ if (state.isTracingDisabled || !state.agentState) {
154
154
  return;
155
155
  }
156
156
 
@@ -190,7 +190,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
190
190
  }
191
191
 
192
192
  export async function createFallbackGenerationFromTurn(event: Record<string, unknown>, message: Record<string, unknown>) {
193
- if (!state.agentState?.root || state.agentState.generationOrder.length > 0) {
193
+ if (state.isTracingDisabled || !state.agentState?.root || state.agentState.generationOrder.length > 0) {
194
194
  return;
195
195
  }
196
196
 
@@ -12,7 +12,7 @@ import {
12
12
  import { MAX_TOOL_PAYLOAD_LENGTH } from "../constants.js";
13
13
 
14
14
  export async function startToolObservation(event: Record<string, unknown>) {
15
- if (!state.agentState?.root) {
15
+ if (state.isTracingDisabled || !state.agentState?.root) {
16
16
  return;
17
17
  }
18
18
 
@@ -59,7 +59,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
59
59
  }
60
60
 
61
61
  export async function finishToolObservation(event: Record<string, unknown>) {
62
- if (!state.agentState) {
62
+ if (state.isTracingDisabled || !state.agentState) {
63
63
  return;
64
64
  }
65
65
 
@@ -119,7 +119,7 @@ export async function finishToolObservation(event: Record<string, unknown>) {
119
119
  }
120
120
 
121
121
  export function closeDanglingObservations(statusMessage: string) {
122
- if (!state.agentState) {
122
+ if (state.isTracingDisabled || !state.agentState) {
123
123
  return;
124
124
  }
125
125
 
@@ -3,7 +3,7 @@ import { getRuntime } from "../langfuse.js";
3
3
  import { shapePayload } from "../utils.js";
4
4
 
5
5
  export async function startTurnObservation(event: Record<string, unknown>) {
6
- if (!state.agentState?.root) {
6
+ if (state.isTracingDisabled || !state.agentState?.root) {
7
7
  return;
8
8
  }
9
9
 
@@ -40,7 +40,7 @@ export async function startTurnObservation(event: Record<string, unknown>) {
40
40
  }
41
41
 
42
42
  export function finishTurnObservation(event?: Record<string, unknown>) {
43
- if (!state.agentState?.activeTurn) {
43
+ if (state.isTracingDisabled || !state.agentState?.activeTurn) {
44
44
  return;
45
45
  }
46
46
 
package/src/langfuse.ts CHANGED
@@ -42,7 +42,8 @@ interface RestFallbackStore {
42
42
  attempted: boolean;
43
43
  }
44
44
 
45
- const OTEL_VISIBILITY_DELAY_MS = 1_500;
45
+ const OTEL_VISIBILITY_TIMEOUT_MS = 1_500;
46
+ const OTEL_VISIBILITY_POLL_INTERVAL_MS = 200;
46
47
 
47
48
  function nowIso() {
48
49
  return new Date().toISOString();
@@ -190,6 +191,23 @@ async function traceExists(rt: LangfuseRuntime, traceId: string): Promise<boolea
190
191
  }
191
192
  }
192
193
 
194
+ async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string): Promise<boolean> {
195
+ const deadline = Date.now() + OTEL_VISIBILITY_TIMEOUT_MS;
196
+
197
+ while (true) {
198
+ if (await traceExists(rt, traceId)) {
199
+ return true;
200
+ }
201
+
202
+ const remainingMs = deadline - Date.now();
203
+ if (remainingMs <= 0) {
204
+ return false;
205
+ }
206
+
207
+ await delay(Math.min(OTEL_VISIBILITY_POLL_INTERVAL_MS, remainingMs));
208
+ }
209
+ }
210
+
193
211
  function eventTimestamp(record: { endTime?: string; startTime?: string; timestamp?: string }) {
194
212
  return record.endTime ?? record.startTime ?? record.timestamp ?? nowIso();
195
213
  }
@@ -201,8 +219,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
201
219
  }
202
220
  store.attempted = true;
203
221
 
204
- await delay(OTEL_VISIBILITY_DELAY_MS);
205
- if (await traceExists(rt, store.trace.id)) {
222
+ if (await waitForTraceVisibility(rt, store.trace.id)) {
206
223
  return;
207
224
  }
208
225
 
package/src/state.ts CHANGED
@@ -1,38 +1,150 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import type { Config, AgentState } from "./types.js";
2
3
 
4
+ export interface SessionRunState {
5
+ currentModel: string;
6
+ currentProvider: string;
7
+ agentState: AgentState | null;
8
+ toolCallCount: number;
9
+ errorCount: number;
10
+ turnCount: number;
11
+ tracingDisabled: boolean;
12
+ }
13
+
14
+ const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
15
+
16
+ let activeSessionId = DEFAULT_SESSION_ID;
17
+ const sessionScope = new AsyncLocalStorage<string>();
18
+
19
+ function createSessionRunState(): SessionRunState {
20
+ return {
21
+ currentModel: "",
22
+ currentProvider: "",
23
+ agentState: null,
24
+ toolCallCount: 0,
25
+ errorCount: 0,
26
+ turnCount: 0,
27
+ tracingDisabled: false,
28
+ };
29
+ }
30
+
31
+ function normalizeSessionId(sessionId?: string) {
32
+ return sessionId || DEFAULT_SESSION_ID;
33
+ }
34
+
35
+ function getActiveSessionId() {
36
+ return sessionScope.getStore() ?? activeSessionId;
37
+ }
38
+
39
+ export function getSessionRunState(sessionId = getActiveSessionId()): SessionRunState {
40
+ const normalizedSessionId = normalizeSessionId(sessionId);
41
+ let sessionState = state.sessionStates.get(normalizedSessionId);
42
+ if (!sessionState) {
43
+ sessionState = createSessionRunState();
44
+ state.sessionStates.set(normalizedSessionId, sessionState);
45
+ }
46
+ return sessionState;
47
+ }
48
+
49
+ export function setCurrentSession(sessionId?: string) {
50
+ activeSessionId = normalizeSessionId(sessionId);
51
+ getSessionRunState(activeSessionId);
52
+ }
53
+
54
+ export function runWithSession<T>(sessionId: string | undefined, fn: () => T): T {
55
+ const normalizedSessionId = normalizeSessionId(sessionId);
56
+ setCurrentSession(normalizedSessionId);
57
+ return sessionScope.run(normalizedSessionId, fn);
58
+ }
59
+
3
60
  export const state = {
4
61
  config: null as Config | null,
5
62
  setupAttemptedThisSession: false,
6
-
7
- currentSessionId: "",
8
- currentModel: "",
9
- currentProvider: "",
10
- agentState: null as AgentState | null,
11
-
12
- // Evaluation tracking state
13
- toolCallCount: 0,
14
- errorCount: 0,
15
- turnCount: 0,
63
+ sessionStates: new Map<string, SessionRunState>(),
64
+
65
+ get currentSessionId() {
66
+ const sessionId = getActiveSessionId();
67
+ return sessionId === DEFAULT_SESSION_ID ? "" : sessionId;
68
+ },
69
+ set currentSessionId(sessionId: string) {
70
+ setCurrentSession(sessionId);
71
+ },
72
+
73
+ get currentModel() {
74
+ return getSessionRunState().currentModel;
75
+ },
76
+ set currentModel(model: string) {
77
+ getSessionRunState().currentModel = model;
78
+ },
79
+
80
+ get currentProvider() {
81
+ return getSessionRunState().currentProvider;
82
+ },
83
+ set currentProvider(provider: string) {
84
+ getSessionRunState().currentProvider = provider;
85
+ },
86
+
87
+ get agentState() {
88
+ return getSessionRunState().agentState;
89
+ },
90
+ set agentState(agentState: AgentState | null) {
91
+ getSessionRunState().agentState = agentState;
92
+ },
93
+
94
+ get toolCallCount() {
95
+ return getSessionRunState().toolCallCount;
96
+ },
97
+ set toolCallCount(toolCallCount: number) {
98
+ getSessionRunState().toolCallCount = toolCallCount;
99
+ },
100
+
101
+ get errorCount() {
102
+ return getSessionRunState().errorCount;
103
+ },
104
+ set errorCount(errorCount: number) {
105
+ getSessionRunState().errorCount = errorCount;
106
+ },
107
+
108
+ get turnCount() {
109
+ return getSessionRunState().turnCount;
110
+ },
111
+ set turnCount(turnCount: number) {
112
+ getSessionRunState().turnCount = turnCount;
113
+ },
114
+
115
+ get isTracingDisabled() {
116
+ return getSessionRunState().tracingDisabled;
117
+ },
118
+ set isTracingDisabled(disabled: boolean) {
119
+ getSessionRunState().tracingDisabled = disabled;
120
+ },
16
121
  };
17
122
 
18
- export function resetRunState() {
19
- state.agentState = null;
20
- state.toolCallCount = 0;
21
- state.errorCount = 0;
22
- state.turnCount = 0;
23
- state.currentModel = "";
24
- state.currentProvider = "";
123
+ export function resetRunState(sessionId = getActiveSessionId()) {
124
+ state.sessionStates.set(normalizeSessionId(sessionId), createSessionRunState());
25
125
  }
26
126
 
27
- export function computeEvaluationScores() {
28
- const toolSuccessRate = state.toolCallCount > 0 ? (state.toolCallCount - state.errorCount) / state.toolCallCount : 1;
29
- const sessionHadErrors = state.errorCount > 0;
127
+ export function clearAllSessionStates() {
128
+ state.sessionStates.clear();
129
+ activeSessionId = DEFAULT_SESSION_ID;
130
+ getSessionRunState();
131
+ }
132
+
133
+ export function computeEvaluationScores(sessionId = getActiveSessionId()) {
134
+ const sessionState = getSessionRunState(sessionId);
135
+ const toolSuccessRate =
136
+ sessionState.toolCallCount > 0
137
+ ? (sessionState.toolCallCount - sessionState.errorCount) / sessionState.toolCallCount
138
+ : 1;
139
+ const sessionHadErrors = sessionState.errorCount > 0;
30
140
 
31
141
  return {
32
- tool_call_count: state.toolCallCount,
33
- turn_count: state.turnCount,
34
- total_tool_errors: state.errorCount,
142
+ tool_call_count: sessionState.toolCallCount,
143
+ turn_count: sessionState.turnCount,
144
+ total_tool_errors: sessionState.errorCount,
35
145
  tool_success_rate: toolSuccessRate,
36
146
  session_had_errors: sessionHadErrors ? 1 : 0,
37
147
  };
38
148
  }
149
+
150
+ getSessionRunState();
package/src/utils.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  MAX_ARRAY_ITEMS,
3
3
  MAX_DEPTH,
4
4
  MAX_OBJECT_KEYS,
5
+ MAX_PAYLOAD_NODES,
5
6
  MAX_STRING_LENGTH,
6
7
  MAX_TOOL_PAYLOAD_LENGTH,
7
8
  } from "./constants.js";
@@ -23,11 +24,25 @@ export function tryParseJson(value: string): unknown {
23
24
  }
24
25
  }
25
26
 
26
- export function shapePayload(value: unknown, options: { maxString?: number; depth?: number } = {}): unknown {
27
+ const PAYLOAD_TOO_LARGE = "[payload too large]";
28
+
29
+ export function shapePayload(value: unknown, options: { maxString?: number; depth?: number; maxNodes?: number } = {}): unknown {
27
30
  const maxString = options.maxString ?? MAX_STRING_LENGTH;
28
31
  const depth = options.depth ?? MAX_DEPTH;
32
+ const maxNodes = options.maxNodes ?? MAX_PAYLOAD_NODES;
33
+ const budget = { exhausted: false, nodeCount: 0 };
29
34
 
30
35
  function visit(item: unknown, remainingDepth: number, seen: WeakSet<object>): unknown {
36
+ if (budget.exhausted) {
37
+ return PAYLOAD_TOO_LARGE;
38
+ }
39
+
40
+ budget.nodeCount++;
41
+ if (budget.nodeCount > maxNodes) {
42
+ budget.exhausted = true;
43
+ return PAYLOAD_TOO_LARGE;
44
+ }
45
+
31
46
  if (typeof item === "string") {
32
47
  const truncated = truncate(item, maxString);
33
48
  const parsed = tryParseJson(truncated);
@@ -59,7 +74,15 @@ export function shapePayload(value: unknown, options: { maxString?: number; dept
59
74
  }
60
75
 
61
76
  if (Array.isArray(item)) {
62
- return item.slice(0, MAX_ARRAY_ITEMS).map((entry) => visit(entry, remainingDepth - 1, seen));
77
+ const output: unknown[] = [];
78
+ const limit = Math.min(item.length, MAX_ARRAY_ITEMS);
79
+ for (let index = 0; index < limit; index++) {
80
+ output.push(visit(item[index], remainingDepth - 1, seen));
81
+ if (budget.exhausted) {
82
+ break;
83
+ }
84
+ }
85
+ return output;
63
86
  }
64
87
 
65
88
  if (item instanceof Error) {
@@ -77,8 +100,16 @@ export function shapePayload(value: unknown, options: { maxString?: number; dept
77
100
  seen.add(item);
78
101
 
79
102
  const output: Record<string, unknown> = {};
80
- for (const [key, entry] of Object.entries(item as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS)) {
81
- output[key] = visit(entry, remainingDepth - 1, seen);
103
+ let keyCount = 0;
104
+ for (const key in item as Record<string, unknown>) {
105
+ if (!Object.hasOwn(item, key)) {
106
+ continue;
107
+ }
108
+ output[key] = visit((item as Record<string, unknown>)[key], remainingDepth - 1, seen);
109
+ keyCount++;
110
+ if (budget.exhausted || keyCount >= MAX_OBJECT_KEYS) {
111
+ break;
112
+ }
82
113
  }
83
114
  return output;
84
115
  }
@@ -0,0 +1,49 @@
1
+ declare module "@opentelemetry/sdk-trace-base" {
2
+ export class BasicTracerProvider {
3
+ constructor(options?: { spanProcessors?: unknown[] });
4
+ forceFlush?(): Promise<void>;
5
+ shutdown?(): Promise<void>;
6
+ }
7
+ }
8
+
9
+ declare module "@langfuse/otel" {
10
+ export class LangfuseSpanProcessor {
11
+ constructor(options: {
12
+ publicKey: string;
13
+ secretKey: string;
14
+ baseUrl: string;
15
+ });
16
+ forceFlush?(): Promise<void>;
17
+ shutdown?(): Promise<void>;
18
+ }
19
+ }
20
+
21
+ declare module "@langfuse/tracing" {
22
+ export function setLangfuseTracerProvider(provider: unknown): void;
23
+
24
+ export function startObservation(
25
+ name: string,
26
+ body?: Record<string, unknown>,
27
+ options?: { asType?: string },
28
+ ): unknown;
29
+
30
+ export function propagateAttributes<T>(
31
+ params: {
32
+ sessionId?: string;
33
+ traceName?: string;
34
+ metadata?: Record<string, string>;
35
+ tags?: string[];
36
+ },
37
+ fn: () => T,
38
+ ): T;
39
+ }
40
+
41
+ declare module "@langfuse/client" {
42
+ export class LangfuseClient {
43
+ constructor(options: {
44
+ publicKey: string;
45
+ secretKey: string;
46
+ baseUrl: string;
47
+ });
48
+ }
49
+ }