pi-langfuse 1.4.0 → 1.4.2

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
@@ -17,7 +17,7 @@ Langfuse provides open-source observability for LLM applications. This extension
17
17
  - **REST fallback for self-hosted Langfuse**: Uses the Langfuse OpenTelemetry SDK first, then verifies that the trace is visible. If a self-hosted OTel ingestion pipeline accepts spans but does not materialize traces, the extension writes the run through Langfuse's REST ingestion API.
18
18
  - **Per-Request Generations**: Records a separate `generation` observation for every provider request, including the actual provider payload instead of only the original prompt.
19
19
  - **Final Message Capture**: Uses finalized assistant messages for generation and root outputs, so Langfuse shows what the user actually saw in Pi.
20
- - **Tool Observability**: Creates Langfuse `tool` observations for every tool call, including arguments, results, and error states.
20
+ - **Tool Observability**: Creates Langfuse `tool` observations for every tool call, including arguments, results, error states, and payload/latency metrics.
21
21
  - **Parallel Tool Safety**: Correlates tool observations by `toolCallId`, avoiding result mix-ups when Pi runs tools concurrently.
22
22
  - **Session Correlation**: Groups traces from the same Pi session under a shared Langfuse session ID.
23
23
  - **Cost and Token Tracking**: Records usage and cost details on each generation when Pi/provider payloads expose them.
@@ -32,6 +32,7 @@ Langfuse provides open-source observability for LLM applications. This extension
32
32
  - The first generation in a tool-using run can show the assistant's tool-call message, the tool observation shows execution I/O, and the follow-up generation shows the final natural-language answer.
33
33
  - Tool failures are marked on the tool observation and reflected in trace-level scores, while later generations still preserve the tool error result in their input history.
34
34
  - Shutdown and interrupted runs flush pending telemetry and mark unfinished observations as cancelled/warning instead of silently losing the trace.
35
+ - Agent-end runtime shutdown is deferred so Langfuse flushing does not block Pi's visible turn completion.
35
36
 
36
37
  ## Prerequisites
37
38
 
@@ -265,6 +266,9 @@ Trace (name: "pi-agent")
265
266
  | `output` | Tool result, shaped and truncated for readability |
266
267
  | `metadata.toolCallId` | Stable Pi tool call identifier |
267
268
  | `metadata.isError` | Whether the tool failed |
269
+ | `metadata.durationMs` | Approximate tool runtime in milliseconds |
270
+ | `metadata.inputBytes` | UTF-8 byte size of the shaped tool input payload |
271
+ | `metadata.outputBytes` | UTF-8 byte size of the shaped tool output payload |
268
272
  | `level` | `ERROR` for failed tool calls, otherwise `DEFAULT` |
269
273
 
270
274
  ### Observation-Level Scores
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,12 +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
- await shutdownRuntime();
132
- });
138
+ setTimeout(() => {
139
+ shutdownRuntime().catch((error) => {
140
+ console.warn("📊 Langfuse: Deferred shutdown failed", error);
141
+ });
142
+ }, 0);
143
+ }));
133
144
 
134
145
  const handleSessionInterruption = (reason: string) => {
135
146
  if (state.agentState?.root) {
@@ -139,15 +150,21 @@ export default async function (pi: ExtensionAPI) {
139
150
  resetRunState();
140
151
  };
141
152
 
142
- pi.on("session_before_switch", async () => {
143
- handleSessionInterruption("Session switched");
153
+ pi.on("session_before_switch", async (_event, ctx) => {
154
+ const sessionId = getSessionId(ctx);
155
+ if (sessionId) {
156
+ setCurrentSession(sessionId);
157
+ }
144
158
  });
145
159
 
146
- pi.on("session_before_fork", async () => {
147
- handleSessionInterruption("Session forked");
160
+ pi.on("session_before_fork", async (_event, ctx) => {
161
+ const sessionId = getSessionId(ctx);
162
+ if (sessionId) {
163
+ setCurrentSession(sessionId);
164
+ }
148
165
  });
149
166
 
150
- pi.on("session_compact", async (event) => {
167
+ pi.on("session_compact", async (event, ctx) => withSession(ctx, async () => {
151
168
  if (state.agentState?.root) {
152
169
  const parent = state.agentState.activeTurn ?? state.agentState.root;
153
170
  try {
@@ -165,10 +182,10 @@ export default async function (pi: ExtensionAPI) {
165
182
  // ignore
166
183
  }
167
184
  }
168
- });
185
+ }));
169
186
 
170
- pi.on("session_shutdown", async () => {
187
+ pi.on("session_shutdown", async (_event, ctx) => withSession(ctx, async () => {
171
188
  handleSessionInterruption("Session shutdown before agent completed");
172
189
  await shutdownRuntime();
173
- });
190
+ }));
174
191
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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
  },
@@ -7,6 +7,7 @@ import {
7
7
  shapePayload,
8
8
  extractTextContent,
9
9
  truncate,
10
+ estimatePayloadBytes,
10
11
  } from "../utils.js";
11
12
  import { MAX_TOOL_PAYLOAD_LENGTH } from "../constants.js";
12
13
 
@@ -22,27 +23,36 @@ export async function startToolObservation(event: Record<string, unknown>) {
22
23
 
23
24
  try {
24
25
  const toolName = getToolName(event);
26
+ const toolInput = getToolInput(event);
27
+ const shapedInput = shapePayload(toolInput, { maxString: MAX_TOOL_PAYLOAD_LENGTH });
28
+ const inputBytes = estimatePayloadBytes(shapedInput, MAX_TOOL_PAYLOAD_LENGTH);
25
29
  const parent = state.agentState.activeTurn ?? state.agentState.root;
26
30
  const tool = parent.startObservation
27
31
  ? parent.startObservation(
28
32
  toolName,
29
33
  {
30
- input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
31
- metadata: { toolName, toolCallId },
34
+ input: shapedInput,
35
+ metadata: { toolName, toolCallId, inputBytes },
32
36
  },
33
37
  { asType: "tool" },
34
38
  )
35
39
  : (await getRuntime()).startObservation(
36
40
  toolName,
37
41
  {
38
- input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
39
- metadata: { toolName, toolCallId },
42
+ input: shapedInput,
43
+ metadata: { toolName, toolCallId, inputBytes },
40
44
  },
41
45
  { asType: "tool" },
42
46
  );
43
47
 
44
48
  state.toolCallCount++;
45
- state.agentState.activeTools.set(toolCallId, { observation: tool, toolName, ended: false });
49
+ state.agentState.activeTools.set(toolCallId, {
50
+ observation: tool,
51
+ toolName,
52
+ ended: false,
53
+ startedAt: Date.now(),
54
+ inputBytes,
55
+ });
46
56
  } catch (e) {
47
57
  console.warn("📊 Langfuse: Failed to start tool observation", e);
48
58
  }
@@ -73,15 +83,22 @@ export async function finishToolObservation(event: Record<string, unknown>) {
73
83
  event;
74
84
 
75
85
  try {
86
+ const shapedOutput = shapePayload(output, { maxString: MAX_TOOL_PAYLOAD_LENGTH });
87
+ const outputBytes = estimatePayloadBytes(shapedOutput, MAX_TOOL_PAYLOAD_LENGTH);
88
+ const durationMs = Math.max(0, Date.now() - activeTool.startedAt);
89
+
76
90
  activeTool.observation
77
91
  .update({
78
- output: shapePayload(output, { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
92
+ output: shapedOutput,
79
93
  level: isError ? "ERROR" : "DEFAULT",
80
94
  statusMessage: isError ? truncate(String(event.error ?? output), 1_000) : undefined,
81
95
  metadata: {
82
96
  toolName: activeTool.toolName,
83
97
  toolCallId,
84
98
  isError,
99
+ durationMs,
100
+ inputBytes: activeTool.inputBytes,
101
+ outputBytes,
85
102
  },
86
103
  })
87
104
  .end();
package/src/state.ts CHANGED
@@ -1,38 +1,141 @@
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
+ }
12
+
13
+ const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
14
+
15
+ let activeSessionId = DEFAULT_SESSION_ID;
16
+ const sessionScope = new AsyncLocalStorage<string>();
17
+
18
+ function createSessionRunState(): SessionRunState {
19
+ return {
20
+ currentModel: "",
21
+ currentProvider: "",
22
+ agentState: null,
23
+ toolCallCount: 0,
24
+ errorCount: 0,
25
+ turnCount: 0,
26
+ };
27
+ }
28
+
29
+ function normalizeSessionId(sessionId?: string) {
30
+ return sessionId || DEFAULT_SESSION_ID;
31
+ }
32
+
33
+ function getActiveSessionId() {
34
+ return sessionScope.getStore() ?? activeSessionId;
35
+ }
36
+
37
+ export function getSessionRunState(sessionId = getActiveSessionId()): SessionRunState {
38
+ const normalizedSessionId = normalizeSessionId(sessionId);
39
+ let sessionState = state.sessionStates.get(normalizedSessionId);
40
+ if (!sessionState) {
41
+ sessionState = createSessionRunState();
42
+ state.sessionStates.set(normalizedSessionId, sessionState);
43
+ }
44
+ return sessionState;
45
+ }
46
+
47
+ export function setCurrentSession(sessionId?: string) {
48
+ activeSessionId = normalizeSessionId(sessionId);
49
+ getSessionRunState(activeSessionId);
50
+ }
51
+
52
+ export function runWithSession<T>(sessionId: string | undefined, fn: () => T): T {
53
+ const normalizedSessionId = normalizeSessionId(sessionId);
54
+ setCurrentSession(normalizedSessionId);
55
+ return sessionScope.run(normalizedSessionId, fn);
56
+ }
57
+
3
58
  export const state = {
4
59
  config: null as Config | null,
5
60
  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,
61
+ sessionStates: new Map<string, SessionRunState>(),
62
+
63
+ get currentSessionId() {
64
+ const sessionId = getActiveSessionId();
65
+ return sessionId === DEFAULT_SESSION_ID ? "" : sessionId;
66
+ },
67
+ set currentSessionId(sessionId: string) {
68
+ setCurrentSession(sessionId);
69
+ },
70
+
71
+ get currentModel() {
72
+ return getSessionRunState().currentModel;
73
+ },
74
+ set currentModel(model: string) {
75
+ getSessionRunState().currentModel = model;
76
+ },
77
+
78
+ get currentProvider() {
79
+ return getSessionRunState().currentProvider;
80
+ },
81
+ set currentProvider(provider: string) {
82
+ getSessionRunState().currentProvider = provider;
83
+ },
84
+
85
+ get agentState() {
86
+ return getSessionRunState().agentState;
87
+ },
88
+ set agentState(agentState: AgentState | null) {
89
+ getSessionRunState().agentState = agentState;
90
+ },
91
+
92
+ get toolCallCount() {
93
+ return getSessionRunState().toolCallCount;
94
+ },
95
+ set toolCallCount(toolCallCount: number) {
96
+ getSessionRunState().toolCallCount = toolCallCount;
97
+ },
98
+
99
+ get errorCount() {
100
+ return getSessionRunState().errorCount;
101
+ },
102
+ set errorCount(errorCount: number) {
103
+ getSessionRunState().errorCount = errorCount;
104
+ },
105
+
106
+ get turnCount() {
107
+ return getSessionRunState().turnCount;
108
+ },
109
+ set turnCount(turnCount: number) {
110
+ getSessionRunState().turnCount = turnCount;
111
+ },
16
112
  };
17
113
 
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 = "";
114
+ export function resetRunState(sessionId = getActiveSessionId()) {
115
+ state.sessionStates.set(normalizeSessionId(sessionId), createSessionRunState());
25
116
  }
26
117
 
27
- export function computeEvaluationScores() {
28
- const toolSuccessRate = state.toolCallCount > 0 ? (state.toolCallCount - state.errorCount) / state.toolCallCount : 1;
29
- const sessionHadErrors = state.errorCount > 0;
118
+ export function clearAllSessionStates() {
119
+ state.sessionStates.clear();
120
+ activeSessionId = DEFAULT_SESSION_ID;
121
+ getSessionRunState();
122
+ }
123
+
124
+ export function computeEvaluationScores(sessionId = getActiveSessionId()) {
125
+ const sessionState = getSessionRunState(sessionId);
126
+ const toolSuccessRate =
127
+ sessionState.toolCallCount > 0
128
+ ? (sessionState.toolCallCount - sessionState.errorCount) / sessionState.toolCallCount
129
+ : 1;
130
+ const sessionHadErrors = sessionState.errorCount > 0;
30
131
 
31
132
  return {
32
- tool_call_count: state.toolCallCount,
33
- turn_count: state.turnCount,
34
- total_tool_errors: state.errorCount,
133
+ tool_call_count: sessionState.toolCallCount,
134
+ turn_count: sessionState.turnCount,
135
+ total_tool_errors: sessionState.errorCount,
35
136
  tool_success_rate: toolSuccessRate,
36
137
  session_had_errors: sessionHadErrors ? 1 : 0,
37
138
  };
38
139
  }
140
+
141
+ getSessionRunState();
package/src/types.ts CHANGED
@@ -87,6 +87,8 @@ export interface ToolState {
87
87
  observation: LangfuseObservation;
88
88
  toolName: string;
89
89
  ended: boolean;
90
+ startedAt: number;
91
+ inputBytes: number;
90
92
  }
91
93
 
92
94
  export interface AgentState {
package/src/utils.ts CHANGED
@@ -97,6 +97,10 @@ export function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGT
97
97
  }
98
98
  }
99
99
 
100
+ export function estimatePayloadBytes(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGTH): number {
101
+ return new TextEncoder().encode(safeSerialize(value, maxLength)).length;
102
+ }
103
+
100
104
  export function extractTextContent(content: unknown, maxLength?: number): string | undefined {
101
105
  if (typeof content === "string") {
102
106
  return maxLength ? truncate(content, maxLength) : content;
@@ -1,140 +0,0 @@
1
- ---
2
- name: langfuse
3
- description: Interact with Langfuse and access its documentation. Use when needing to (1) query or modify Langfuse data programmatically via the CLI — traces, prompts, datasets, scores, sessions, and any other API resource, (2) look up Langfuse documentation, concepts, integration guides, or SDK usage, or (3) understand how any Langfuse feature works. This skill covers CLI-based API access (via npx) and multiple documentation retrieval methods.
4
- allowed-tools:
5
- - WebFetch(domain:langfuse.com)
6
- - Bash(curl *langfuse.com/*)
7
- - Bash(npx langfuse-cli api __schema *)
8
- - Bash(npx langfuse-cli api * --help *)
9
- - Bash(npx langfuse-cli api * list *)
10
- - Bash(npx langfuse-cli api * get *)
11
- - Bash(bunx langfuse-cli api __schema *)
12
- - Bash(bunx langfuse-cli api * --help *)
13
- - Bash(bunx langfuse-cli api * list *)
14
- - Bash(bunx langfuse-cli api * get *)
15
- ---
16
-
17
- # Langfuse
18
-
19
- This skill helps you use Langfuse effectively across all common workflows: instrumenting applications, migrating prompts, debugging traces, and accessing data programmatically.
20
-
21
- ## Core Principles
22
-
23
- Follow these principles for ALL Langfuse work:
24
-
25
- 1. **Documentation First**: NEVER implement based on memory. Always fetch current docs before writing code (Langfuse updates frequently) See the section below on how to access documentation.
26
- 2. **CLI for Data Access**: Use `langfuse-cli` when querying/modifying Langfuse data. See the section below on how to use the CLI.
27
- 3. **Best Practices by Use Case**: Check the relevant reference file below for use-case-specific guidelines before implementing
28
- 4. **Use latest Langfuse versions**: Unless the user specified otherwise or there's a good reason, always use the latest version of Langfuse SDKs/APIs.
29
-
30
-
31
- ## Use case specific references
32
-
33
- - instrumenting an existing function/application: references/instrumentation.md
34
- - migrating prompts from a codebase into Langfuse: references/prompt-migration.md
35
- - capturing user feedback (thumbs, ratings, implicit signals) as scores on traces: references/user-feedback.md
36
- - further tips on using the Langfuse CLI: references/cli.md
37
- - upgrading or migrating Langfuse SDKs to the latest version: references/sdk-upgrade.md
38
- - systematic error analysis — reading traces, building failure taxonomy, deciding what to fix: references/error-analysis.md
39
- - submitting feedback about this skill: references/skill-feedback.md
40
-
41
- ## 1. Langfuse API via CLI
42
-
43
- Use the `langfuse-cli` to interact with the full Langfuse REST API from the command line. Run via npx (no install required):
44
-
45
- Start by discovering the schema and available arguments:
46
-
47
- ```bash
48
- # Discover all available resources
49
- npx langfuse-cli api __schema
50
-
51
- # List actions for a resource
52
- npx langfuse-cli api <resource> --help
53
-
54
- # Show args/options for a specific action
55
- npx langfuse-cli api <resource> <action> --help
56
- ```
57
-
58
- ### Credentials
59
-
60
- Set environment variables before making calls:
61
-
62
- ```bash
63
- export LANGFUSE_PUBLIC_KEY=pk-lf-...
64
- export LANGFUSE_SECRET_KEY=sk-lf-...
65
- export LANGFUSE_HOST=https://cloud.langfuse.com # example for EU cloud. For US cloud it's us.cloud.langfuse.com, and can also be a self-hosted URL. The server must always be specified in order to access Langfuse.
66
- ```
67
-
68
- If not set, ask the user to set them in their shell or a `.env` file (do not ask them to paste keys into chat for security reasons). Keys are found in Langfuse UI → Settings → API Keys.
69
-
70
- ### Detailed CLI Reference
71
-
72
- For common workflows, tips, and full usage patterns, see [references/cli.md](references/cli.md).
73
-
74
- ## 2. Langfuse Documentation
75
-
76
- Three methods to access Langfuse docs, in order of preference. **Always prefer your application's native web fetch and search tools** (e.g., `WebFetch`, `WebSearch`, `mcp_fetch`, etc.) over `curl` when available. The URLs and patterns below work with any fetching method — the `curl` examples are just illustrative.
77
-
78
- ### 2a. Documentation Index (llms.txt)
79
-
80
- Fetch the full index of all documentation pages:
81
-
82
- ```bash
83
- curl -s https://langfuse.com/llms.txt
84
- ```
85
-
86
- Returns a structured list of every doc page with titles and URLs. Use this to discover the right page for a topic, then fetch that page directly.
87
-
88
- Alternatively, you can start on `https://langfuse.com/docs` and explore the site to find the page you need.
89
-
90
- ### 2b. Fetch Individual Pages as Markdown
91
-
92
- Any page listed in llms.txt can be fetched as markdown by appending `.md` to its path or by using `Accept: text/markdown` in the request headers. Use this when you know which page contains the information needed. Returns clean markdown with code examples and configuration details.
93
-
94
- ```bash
95
- curl -s "https://langfuse.com/docs/observability/overview.md"
96
- curl -s "https://langfuse.com/docs/observability/overview" -H "Accept: text/markdown"
97
- ```
98
-
99
- ### 2c. Search Documentation
100
-
101
- When you need to find information across all docs and github issues/discussions without knowing the specific page:
102
-
103
- ```bash
104
- curl -s "https://langfuse.com/api/search-docs?query=<url-encoded-query>"
105
- ```
106
-
107
- Example:
108
-
109
- ```bash
110
- curl -s "https://langfuse.com/api/search-docs?query=How+do+I+trace+LangGraph+agents"
111
- ```
112
-
113
- Returns a JSON response with:
114
-
115
- - `query`: the original query
116
- - `answer`: a JSON string containing an array of matching documents, each with:
117
- - `url`: link to the doc page
118
- - `title`: page title
119
- - `source.content`: array of relevant text excerpts from the page
120
-
121
- Search is a great fallback if you cannot find the relevant pages or need more context. Especially useful when debugging issues as all GitHub Issues and Discussions are also indexed. Responses can be large — extract only the relevant portions.
122
-
123
- ### Documentation Workflow
124
-
125
- 1. Start with **llms.txt** to orient — scan for relevant page titles
126
- 2. **Fetch specific pages** when you identify the right one
127
- 3. Fall back to **search** when the topic is unclear and you want more context
128
-
129
- ## Skill Feedback
130
-
131
- When the user expresses that something about this skill is not working as expected, gives incorrect guidance, is missing information, or could be improved — offer to submit feedback to the Langfuse skill maintainers. This includes when:
132
-
133
- - The skill gave wrong or outdated instructions
134
- - A workflow didn't produce the expected result
135
- - The user wishes the skill covered something it doesn't
136
- - The user explicitly says something like "this should work differently" or "this is wrong"
137
-
138
- **Do NOT trigger this** for issues with Langfuse itself (the product) — only for issues with this skill's instructions and behavior.
139
-
140
- When triggered, follow the process in [references/skill-feedback.md](references/skill-feedback.md).