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 +5 -1
- package/index.ts +5 -1
- package/package.json +1 -1
- package/src/handlers/tool.ts +23 -6
- package/src/types.ts +2 -0
- package/src/utils.ts +4 -0
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
|
|
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
|
-
|
|
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
package/src/handlers/tool.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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, {
|
|
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:
|
|
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
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;
|