pi-langfuse 1.4.0 → 1.4.1

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
@@ -128,7 +128,11 @@ export default async function (pi: ExtensionAPI) {
128
128
 
129
129
  pi.on("agent_end", async (event) => {
130
130
  await finishAgentRun(event);
131
- await shutdownRuntime();
131
+ setTimeout(() => {
132
+ shutdownRuntime().catch((error) => {
133
+ console.warn("📊 Langfuse: Deferred shutdown failed", error);
134
+ });
135
+ }, 0);
132
136
  });
133
137
 
134
138
  const handleSessionInterruption = (reason: string) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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/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;