pi-langfuse 1.5.18 → 1.6.0
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 +6 -0
- package/README_CN.md +6 -0
- package/index.ts +52 -49
- package/package.json +3 -3
- package/src/handlers/agent.ts +102 -1
- package/src/handlers/cache.ts +147 -0
- package/src/handlers/generation.ts +39 -2
- package/src/handlers/session.ts +90 -0
- package/src/handlers/system-state.ts +186 -0
- package/src/handlers/tool.ts +1 -1
- package/src/handlers/turn.ts +1 -1
- package/src/langfuse.ts +76 -20
- package/src/observation.ts +1 -1
- package/src/state.ts +18 -3
- package/src/types.ts +21 -2
- package/src/utils.ts +75 -0
- package/types/langfuse-runtime-shims.d.ts +0 -49
- package/types/node-shims.d.ts +0 -29
- package/types/pi-coding-agent.d.ts +0 -16
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { state } from "../state.js";
|
|
5
|
+
import {
|
|
6
|
+
extractCostDetails,
|
|
7
|
+
extractUsage,
|
|
8
|
+
getCapturePolicy,
|
|
9
|
+
shapePayload,
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
|
|
12
|
+
type RecordLike = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function namesFromTools(value: unknown): string[] {
|
|
15
|
+
if (!Array.isArray(value)) return [];
|
|
16
|
+
return value.flatMap((tool) => {
|
|
17
|
+
if (typeof tool === "string") return [tool];
|
|
18
|
+
if (!tool || typeof tool !== "object") return [];
|
|
19
|
+
const name = (tool as RecordLike).name;
|
|
20
|
+
return typeof name === "string" ? [name] : [];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function recordSessionCompaction(event: RecordLike): Promise<void> {
|
|
25
|
+
const agent = state.agentState;
|
|
26
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
27
|
+
|
|
28
|
+
const entry = event.compactionEntry && typeof event.compactionEntry === "object"
|
|
29
|
+
? event.compactionEntry as RecordLike
|
|
30
|
+
: {};
|
|
31
|
+
const checkpoint = entry.systemMessage && typeof entry.systemMessage === "object"
|
|
32
|
+
? entry.systemMessage as RecordLike
|
|
33
|
+
: undefined;
|
|
34
|
+
const checkpointTools = namesFromTools(checkpoint?.toolsAdded);
|
|
35
|
+
const checkpointSections = checkpoint?.sections && typeof checkpoint.sections === "object"
|
|
36
|
+
? Object.keys(checkpoint.sections as RecordLike)
|
|
37
|
+
: [];
|
|
38
|
+
const captured = applyCapturePolicy(
|
|
39
|
+
{
|
|
40
|
+
input: shapePayload({ summary: entry.summary }),
|
|
41
|
+
metadata: {
|
|
42
|
+
reason: event.reason,
|
|
43
|
+
willRetry: event.willRetry,
|
|
44
|
+
fromExtension: event.fromExtension,
|
|
45
|
+
compactionEntryId: entry.id,
|
|
46
|
+
firstKeptEntryId: entry.firstKeptEntryId,
|
|
47
|
+
tokensBefore: entry.tokensBefore,
|
|
48
|
+
checkpointSectionNames: checkpointSections,
|
|
49
|
+
checkpointToolNames: checkpointTools,
|
|
50
|
+
checkpointToolCount: checkpointTools.length,
|
|
51
|
+
promptStateHash: agent.promptStateHash,
|
|
52
|
+
toolStateHash: agent.toolStateHash,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
getCapturePolicy(),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const parent = agent.activeTurn ?? agent.activeAttempt ?? agent.root;
|
|
60
|
+
const compaction = await startChildObservation({
|
|
61
|
+
parent,
|
|
62
|
+
runtime: getRuntime,
|
|
63
|
+
name: "session-compaction",
|
|
64
|
+
body: { input: captured.input, metadata: captured.metadata },
|
|
65
|
+
asType: "span",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (entry.usage && typeof entry.usage === "object") {
|
|
69
|
+
const summary = applyCapturePolicy({ output: entry.summary }, getCapturePolicy());
|
|
70
|
+
const costDetails = extractCostDetails({ usage: entry.usage });
|
|
71
|
+
const generation = await startChildObservation({
|
|
72
|
+
parent: compaction,
|
|
73
|
+
runtime: getRuntime,
|
|
74
|
+
name: "compaction-summary",
|
|
75
|
+
body: {
|
|
76
|
+
output: summary.output,
|
|
77
|
+
model: state.currentModel || undefined,
|
|
78
|
+
usageDetails: extractUsage({ usage: entry.usage }),
|
|
79
|
+
...(costDetails ? { costDetails } : {}),
|
|
80
|
+
metadata: { provider: state.currentProvider || undefined },
|
|
81
|
+
},
|
|
82
|
+
asType: "generation",
|
|
83
|
+
});
|
|
84
|
+
generation.end();
|
|
85
|
+
}
|
|
86
|
+
compaction.end();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn("📊 Langfuse: Failed to record session compaction", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
4
|
+
import { getRuntime } from "../langfuse.js";
|
|
5
|
+
import { startChildObservation } from "../observation.js";
|
|
6
|
+
import { getSessionRunState, state } from "../state.js";
|
|
7
|
+
import { getCapturePolicy, shapePayload } from "../utils.js";
|
|
8
|
+
|
|
9
|
+
type RecordLike = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function fingerprint(value: string): string {
|
|
12
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toolName(tool: unknown): string | undefined {
|
|
16
|
+
if (typeof tool === "string") return tool;
|
|
17
|
+
if (!tool || typeof tool !== "object") return undefined;
|
|
18
|
+
const name = (tool as RecordLike).name;
|
|
19
|
+
return typeof name === "string" && name ? name : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sortedUnique(values: Iterable<string>): string[] {
|
|
23
|
+
return [...new Set(values)].sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readNewSystemMessages(ctx: any): Array<{ entryId: string; message: RecordLike }> {
|
|
27
|
+
const session = getSessionRunState();
|
|
28
|
+
let branch: unknown;
|
|
29
|
+
try {
|
|
30
|
+
branch = ctx?.sessionManager?.getBranch?.();
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(branch)) return [];
|
|
35
|
+
|
|
36
|
+
const messages: Array<{ entryId: string; message: RecordLike }> = [];
|
|
37
|
+
for (const rawEntry of branch) {
|
|
38
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
39
|
+
const entry = rawEntry as RecordLike;
|
|
40
|
+
const message = entry.message;
|
|
41
|
+
if (entry.type !== "message" || !message || typeof message !== "object") continue;
|
|
42
|
+
if ((message as RecordLike).role !== "system") continue;
|
|
43
|
+
const entryId = typeof entry.id === "string" ? entry.id : "";
|
|
44
|
+
if (!entryId || session.seenSystemEntryIds.has(entryId)) continue;
|
|
45
|
+
session.seenSystemEntryIds.add(entryId);
|
|
46
|
+
messages.push({ entryId, message: message as RecordLike });
|
|
47
|
+
}
|
|
48
|
+
return messages;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mark restored transcript state as already seen so the first trace emits one snapshot, not the full history. */
|
|
52
|
+
export function initializeSystemStateTracking(ctx: any): void {
|
|
53
|
+
const session = getSessionRunState();
|
|
54
|
+
try {
|
|
55
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
56
|
+
if (!Array.isArray(branch)) return;
|
|
57
|
+
for (const rawEntry of branch) {
|
|
58
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
59
|
+
const entry = rawEntry as RecordLike;
|
|
60
|
+
const message = entry.message;
|
|
61
|
+
if (
|
|
62
|
+
entry.type === "message" &&
|
|
63
|
+
typeof entry.id === "string" &&
|
|
64
|
+
message &&
|
|
65
|
+
typeof message === "object" &&
|
|
66
|
+
(message as RecordLike).role === "system"
|
|
67
|
+
) {
|
|
68
|
+
session.seenSystemEntryIds.add(entry.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Session history is optional in SDK/headless hosts.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function recordSystemState(ctx: any, activeTools: string[]): Promise<void> {
|
|
77
|
+
const agent = state.agentState;
|
|
78
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
79
|
+
|
|
80
|
+
let prompt = "";
|
|
81
|
+
try {
|
|
82
|
+
prompt = String((await ctx?.getSystemPrompt?.()) ?? "");
|
|
83
|
+
} catch {
|
|
84
|
+
// The tool state is still useful when a host cannot expose the rendered prompt.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tools = sortedUnique(activeTools);
|
|
88
|
+
const promptStateHash = prompt ? fingerprint(prompt) : undefined;
|
|
89
|
+
const toolStateHash = fingerprint(JSON.stringify(tools));
|
|
90
|
+
const session = getSessionRunState();
|
|
91
|
+
const systemMessages = readNewSystemMessages(ctx);
|
|
92
|
+
|
|
93
|
+
const transcriptToolsAdded: string[] = [];
|
|
94
|
+
const transcriptToolsRemoved: string[] = [];
|
|
95
|
+
const sectionsChanged = new Set<string>();
|
|
96
|
+
const sectionsRemoved = new Set<string>();
|
|
97
|
+
const entryIds: string[] = [];
|
|
98
|
+
for (const { entryId, message } of systemMessages) {
|
|
99
|
+
entryIds.push(entryId);
|
|
100
|
+
const sections = message.sections;
|
|
101
|
+
if (sections && typeof sections === "object" && !Array.isArray(sections)) {
|
|
102
|
+
for (const [name, value] of Object.entries(sections as RecordLike)) {
|
|
103
|
+
(value === null ? sectionsRemoved : sectionsChanged).add(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(message.toolsAdded)) {
|
|
107
|
+
for (const tool of message.toolsAdded) {
|
|
108
|
+
const name = toolName(tool);
|
|
109
|
+
if (name) transcriptToolsAdded.push(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(message.toolsRemoved)) {
|
|
113
|
+
for (const tool of message.toolsRemoved) {
|
|
114
|
+
const name = toolName(tool);
|
|
115
|
+
if (name) transcriptToolsRemoved.push(name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const previousTools = new Set(session.lastActiveTools);
|
|
121
|
+
const currentTools = new Set(tools);
|
|
122
|
+
const toolsAdded = sortedUnique([
|
|
123
|
+
...tools.filter((name) => !previousTools.has(name)),
|
|
124
|
+
...transcriptToolsAdded,
|
|
125
|
+
]);
|
|
126
|
+
const toolsRemoved = sortedUnique([
|
|
127
|
+
...session.lastActiveTools.filter((name) => !currentTools.has(name)),
|
|
128
|
+
...transcriptToolsRemoved,
|
|
129
|
+
]);
|
|
130
|
+
const promptChanged = session.lastPromptStateHash !== promptStateHash;
|
|
131
|
+
const toolsChanged = session.lastToolStateHash !== toolStateHash;
|
|
132
|
+
const isInitial = session.lastPromptStateHash === undefined && session.lastToolStateHash === undefined;
|
|
133
|
+
const changed = isInitial || promptChanged || toolsChanged || systemMessages.length > 0;
|
|
134
|
+
if (changed) session.systemStateSequence++;
|
|
135
|
+
|
|
136
|
+
const capturePolicy = getCapturePolicy();
|
|
137
|
+
const rawSystemUpdate = systemMessages.length > 0
|
|
138
|
+
? systemMessages.map(({ entryId, message }) => ({ entryId, ...message }))
|
|
139
|
+
: { role: "system", content: prompt };
|
|
140
|
+
const captured = applyCapturePolicy(
|
|
141
|
+
{
|
|
142
|
+
systemPrompt: shapePayload(rawSystemUpdate),
|
|
143
|
+
metadata: {
|
|
144
|
+
stateSequence: session.systemStateSequence,
|
|
145
|
+
changeKind: isInitial ? "initial" : changed ? "updated" : "unchanged",
|
|
146
|
+
source: systemMessages.length > 0 ? "transcript" : "effective-snapshot",
|
|
147
|
+
transcriptEntryIds: entryIds,
|
|
148
|
+
promptStateHash,
|
|
149
|
+
toolStateHash,
|
|
150
|
+
promptChars: prompt.length,
|
|
151
|
+
activeToolCount: tools.length,
|
|
152
|
+
activeTools: tools,
|
|
153
|
+
toolsAdded,
|
|
154
|
+
toolsRemoved,
|
|
155
|
+
sectionsChanged: sortedUnique(sectionsChanged),
|
|
156
|
+
sectionsRemoved: sortedUnique(sectionsRemoved),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
capturePolicy,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const observation = await startChildObservation({
|
|
164
|
+
parent: agent.activeAttempt ?? agent.root,
|
|
165
|
+
runtime: getRuntime,
|
|
166
|
+
name: "system-state",
|
|
167
|
+
body: {
|
|
168
|
+
input: captured.systemPrompt,
|
|
169
|
+
metadata: captured.metadata,
|
|
170
|
+
},
|
|
171
|
+
asType: "event",
|
|
172
|
+
});
|
|
173
|
+
observation.end();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn("📊 Langfuse: Failed to record system state", error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (changed) agent.systemStateChangeCount = (agent.systemStateChangeCount ?? 0) + 1;
|
|
179
|
+
agent.promptStateHash = promptStateHash;
|
|
180
|
+
agent.toolStateHash = toolStateHash;
|
|
181
|
+
agent.systemStateSequence = session.systemStateSequence;
|
|
182
|
+
agent.activeToolCount = tools.length;
|
|
183
|
+
session.lastPromptStateHash = promptStateHash;
|
|
184
|
+
session.lastToolStateHash = toolStateHash;
|
|
185
|
+
session.lastActiveTools = tools;
|
|
186
|
+
}
|
package/src/handlers/tool.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
37
37
|
getCapturePolicy(),
|
|
38
38
|
);
|
|
39
39
|
const inputBytes = estimatePayloadBytes(captured.toolInput, getLimits().maxToolPayload);
|
|
40
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
40
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
41
41
|
const tool = await startChildObservation({
|
|
42
42
|
parent,
|
|
43
43
|
runtime: getRuntime,
|
package/src/handlers/turn.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function startTurnObservation(event: Record<string, unknown>) {
|
|
|
25
25
|
getCapturePolicy(),
|
|
26
26
|
);
|
|
27
27
|
const observation = await startChildObservation({
|
|
28
|
-
parent: state.agentState.root,
|
|
28
|
+
parent: state.agentState.activeAttempt ?? state.agentState.root,
|
|
29
29
|
runtime: getRuntime,
|
|
30
30
|
name: "turn",
|
|
31
31
|
body: {
|
package/src/langfuse.ts
CHANGED
|
@@ -463,25 +463,73 @@ function observationType(asType?: string): FallbackObservationType {
|
|
|
463
463
|
* `LangfuseSpanProcessor.onStart(span, parentContext)`, reading them from the
|
|
464
464
|
* OTel context that was active when the span was created. `propagateAttributes`
|
|
465
465
|
* only seeds that context for the duration of its callback, so a child created
|
|
466
|
-
* later — from a separate event handler, outside the callback — is written
|
|
467
|
-
*
|
|
466
|
+
* later — from a separate event handler, outside the callback — is written
|
|
467
|
+
* without `session.id`, `langfuse.trace.name`, or `langfuse.trace.metadata.*`.
|
|
468
468
|
*
|
|
469
|
-
* That is invisible in the legacy data model, where
|
|
470
|
-
*
|
|
471
|
-
* session
|
|
472
|
-
* `
|
|
473
|
-
*
|
|
469
|
+
* That is invisible in the legacy data model, where those live on the trace,
|
|
470
|
+
* but Langfuse v4 stores them per event row and filters and aggregates over
|
|
471
|
+
* those rows: a session is `WHERE session_id != ''`, and cost by trace name
|
|
472
|
+
* matches `trace_name` on the generation itself. Unstamped children drop out
|
|
473
|
+
* of `sumMap(cost_details)` and `sumMap(usage_details)`, which is why such
|
|
474
|
+
* sessions and trace-name filters report a correct trace count but no cost
|
|
475
|
+
* and no usage at all.
|
|
474
476
|
*
|
|
475
|
-
* Re-entering the propagated context for every child keeps the whole tree
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
477
|
+
* Re-entering the propagated context for every child keeps the whole tree
|
|
478
|
+
* queryable. The attributes are read back off the parent span rather than
|
|
479
|
+
* from state, so a child created outside an active session scope still
|
|
480
|
+
* inherits whatever its parent was actually stamped with, and grandchildren
|
|
481
|
+
* inherit transitively.
|
|
482
|
+
*
|
|
483
|
+
* The re-entry runs on the OTel root context, not the ambient one.
|
|
484
|
+
* `propagateAttributes` starts from `context.active()`, merges `metadata`
|
|
485
|
+
* and `tags` with whatever that context already carries, and stamps the
|
|
486
|
+
* active span if there is one. Any attributes propagated by the caller —
|
|
487
|
+
* another instrumentation, a foreign `propagateAttributes` scope — would
|
|
488
|
+
* otherwise leak into the child or onto an unrelated span. The parent is
|
|
489
|
+
* still explicit: `observation.startObservation` passes the parent span
|
|
490
|
+
* context to the child, so a clean ambient context changes nothing else.
|
|
479
491
|
*/
|
|
480
|
-
|
|
492
|
+
type PropagatedAttributes = Parameters<LangfuseRuntime["propagateAttributes"]>[0];
|
|
493
|
+
|
|
494
|
+
const PROPAGATED_STRING_ATTRIBUTES = {
|
|
495
|
+
sessionId: "session.id",
|
|
496
|
+
userId: "user.id",
|
|
497
|
+
traceName: "langfuse.trace.name",
|
|
498
|
+
version: "langfuse.version",
|
|
499
|
+
} as const;
|
|
500
|
+
const OTEL_TRACE_TAGS_ATTRIBUTE = "langfuse.trace.tags";
|
|
501
|
+
const OTEL_TRACE_METADATA_PREFIX = "langfuse.trace.metadata.";
|
|
502
|
+
|
|
503
|
+
function readPropagatedAttributes(observation: any): PropagatedAttributes {
|
|
504
|
+
const attributes: Record<string, unknown> = observation?.otelSpan?.attributes ?? {};
|
|
505
|
+
const propagated: PropagatedAttributes = {};
|
|
506
|
+
|
|
507
|
+
for (const [key, attribute] of Object.entries(PROPAGATED_STRING_ATTRIBUTES)) {
|
|
508
|
+
const value = attributes[attribute];
|
|
509
|
+
if (typeof value === "string" && value) {
|
|
510
|
+
propagated[key as keyof typeof PROPAGATED_STRING_ATTRIBUTES] = value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const tags = attributes[OTEL_TRACE_TAGS_ATTRIBUTE];
|
|
515
|
+
if (Array.isArray(tags)) {
|
|
516
|
+
const validTags = tags.filter((tag): tag is string => typeof tag === "string" && tag.length > 0);
|
|
517
|
+
if (validTags.length > 0) {
|
|
518
|
+
propagated.tags = validTags;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
481
521
|
|
|
482
|
-
|
|
483
|
-
const value
|
|
484
|
-
|
|
522
|
+
const metadata: Record<string, string> = {};
|
|
523
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
524
|
+
if (key.startsWith(OTEL_TRACE_METADATA_PREFIX) && typeof value === "string") {
|
|
525
|
+
metadata[key.slice(OTEL_TRACE_METADATA_PREFIX.length)] = value;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (Object.keys(metadata).length > 0) {
|
|
529
|
+
propagated.metadata = metadata;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return propagated;
|
|
485
533
|
}
|
|
486
534
|
|
|
487
535
|
function wrapObservation(
|
|
@@ -543,12 +591,19 @@ function wrapObservation(
|
|
|
543
591
|
return observation.end();
|
|
544
592
|
},
|
|
545
593
|
startObservation(childName: string, childBody?: Record<string, unknown>, options?: { asType?: string }) {
|
|
546
|
-
const
|
|
594
|
+
const inherited = readPropagatedAttributes(observation);
|
|
595
|
+
const fallbackSessionId = state.currentSessionId?.slice(0, 200);
|
|
596
|
+
if (!inherited.sessionId && fallbackSessionId) {
|
|
597
|
+
inherited.sessionId = fallbackSessionId;
|
|
598
|
+
}
|
|
547
599
|
const propagate = runtime?.propagateAttributes;
|
|
548
600
|
const createChild = () => observation.startObservation(childName, childBody, options);
|
|
549
|
-
const
|
|
550
|
-
?
|
|
551
|
-
: createChild
|
|
601
|
+
const createInheritingChild = propagate && Object.keys(inherited).length > 0
|
|
602
|
+
? () => propagate(inherited, createChild)
|
|
603
|
+
: createChild;
|
|
604
|
+
const child = runtime?.withRootContext
|
|
605
|
+
? runtime.withRootContext(createInheritingChild)
|
|
606
|
+
: createInheritingChild();
|
|
552
607
|
return wrapObservation(child, store, childName, childBody, options?.asType, id);
|
|
553
608
|
},
|
|
554
609
|
setTraceIO(traceBody?: { input?: unknown; output?: unknown }) {
|
|
@@ -777,7 +832,7 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
777
832
|
const [
|
|
778
833
|
{ BasicTracerProvider },
|
|
779
834
|
{ resources },
|
|
780
|
-
{ context },
|
|
835
|
+
{ context, ROOT_CONTEXT },
|
|
781
836
|
{ AsyncHooksContextManager },
|
|
782
837
|
{ LangfuseSpanProcessor },
|
|
783
838
|
tracing,
|
|
@@ -822,6 +877,7 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
822
877
|
return wrapObservation(observation, restFallback, name, body, options?.asType);
|
|
823
878
|
}) as unknown as LangfuseRuntime["startObservation"],
|
|
824
879
|
propagateAttributes: tracing.propagateAttributes as unknown as LangfuseRuntime["propagateAttributes"],
|
|
880
|
+
withRootContext: (fn) => context.with(ROOT_CONTEXT, fn),
|
|
825
881
|
scoreClient: new LangfuseClient({
|
|
826
882
|
publicKey: state.config.publicKey,
|
|
827
883
|
secretKey: state.config.secretKey,
|
package/src/observation.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function startChildObservation({
|
|
|
11
11
|
runtime: () => Promise<LangfuseRuntime>;
|
|
12
12
|
name: string;
|
|
13
13
|
body?: ObservationUpdate;
|
|
14
|
-
asType: "generation" | "tool" | "span";
|
|
14
|
+
asType: "event" | "generation" | "tool" | "span";
|
|
15
15
|
}): Promise<LangfuseObservation> {
|
|
16
16
|
if (parent.startObservation) {
|
|
17
17
|
return parent.startObservation(name, body, { asType });
|
package/src/state.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface SessionRunState {
|
|
|
10
10
|
turnCount: number;
|
|
11
11
|
tracingDisabled: boolean;
|
|
12
12
|
setupAttemptedThisSession: boolean;
|
|
13
|
+
lastPromptStateHash?: string;
|
|
14
|
+
lastToolStateHash?: string;
|
|
15
|
+
lastActiveTools: string[];
|
|
16
|
+
systemStateSequence: number;
|
|
17
|
+
seenSystemEntryIds: Set<string>;
|
|
18
|
+
seenUsageEntryIds: Set<string>;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
|
|
@@ -27,6 +33,10 @@ function createSessionRunState(): SessionRunState {
|
|
|
27
33
|
turnCount: 0,
|
|
28
34
|
tracingDisabled: false,
|
|
29
35
|
setupAttemptedThisSession: false,
|
|
36
|
+
lastActiveTools: [],
|
|
37
|
+
systemStateSequence: 0,
|
|
38
|
+
seenSystemEntryIds: new Set(),
|
|
39
|
+
seenUsageEntryIds: new Set(),
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -130,11 +140,16 @@ export const state = {
|
|
|
130
140
|
|
|
131
141
|
export function resetRunState(sessionId = getActiveSessionId()) {
|
|
132
142
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
133
|
-
const
|
|
134
|
-
state.sessionStates.get(normalizedSessionId)?.setupAttemptedThisSession ?? false;
|
|
143
|
+
const previous = state.sessionStates.get(normalizedSessionId);
|
|
135
144
|
state.sessionStates.set(normalizedSessionId, {
|
|
136
145
|
...createSessionRunState(),
|
|
137
|
-
setupAttemptedThisSession,
|
|
146
|
+
setupAttemptedThisSession: previous?.setupAttemptedThisSession ?? false,
|
|
147
|
+
lastPromptStateHash: previous?.lastPromptStateHash,
|
|
148
|
+
lastToolStateHash: previous?.lastToolStateHash,
|
|
149
|
+
lastActiveTools: previous?.lastActiveTools ?? [],
|
|
150
|
+
systemStateSequence: previous?.systemStateSequence ?? 0,
|
|
151
|
+
seenSystemEntryIds: previous?.seenSystemEntryIds ?? new Set(),
|
|
152
|
+
seenUsageEntryIds: previous?.seenUsageEntryIds ?? new Set(),
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
|
package/src/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface LangfuseObservation {
|
|
|
19
19
|
startObservation?(
|
|
20
20
|
name: string,
|
|
21
21
|
body?: ObservationUpdate,
|
|
22
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
22
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
23
23
|
): LangfuseObservation;
|
|
24
24
|
setTraceIO?(body?: { input?: unknown; output?: unknown }): void;
|
|
25
25
|
}
|
|
@@ -82,17 +82,25 @@ export interface LangfuseRuntime {
|
|
|
82
82
|
startObservation: (
|
|
83
83
|
name: string,
|
|
84
84
|
body?: ObservationUpdate,
|
|
85
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
85
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
86
86
|
) => LangfuseObservation;
|
|
87
87
|
propagateAttributes: (
|
|
88
88
|
params: {
|
|
89
89
|
sessionId?: string;
|
|
90
|
+
userId?: string;
|
|
90
91
|
traceName?: string;
|
|
92
|
+
version?: string;
|
|
91
93
|
metadata?: Record<string, string>;
|
|
92
94
|
tags?: string[];
|
|
93
95
|
},
|
|
94
96
|
fn: () => LangfuseObservation,
|
|
95
97
|
) => LangfuseObservation;
|
|
98
|
+
/**
|
|
99
|
+
* Runs `fn` on the OTel root context, so the observations it creates take
|
|
100
|
+
* their propagated attributes only from what is passed explicitly and not
|
|
101
|
+
* from whatever the caller's ambient context happens to carry.
|
|
102
|
+
*/
|
|
103
|
+
withRootContext?: <T>(fn: () => T) => T;
|
|
96
104
|
scoreClient: LangfuseScoreClient;
|
|
97
105
|
spanProcessor?: { forceFlush?: () => Promise<void>; shutdown?: () => Promise<void> };
|
|
98
106
|
tracerProvider?: { forceFlush?: () => Promise<void>; shutdown?: () => Promise<void> };
|
|
@@ -128,6 +136,7 @@ export interface ToolState {
|
|
|
128
136
|
|
|
129
137
|
export interface AgentState {
|
|
130
138
|
root?: LangfuseObservation;
|
|
139
|
+
activeAttempt?: LangfuseObservation;
|
|
131
140
|
activeTurn?: LangfuseObservation;
|
|
132
141
|
traceId?: string;
|
|
133
142
|
promptInput?: unknown;
|
|
@@ -139,4 +148,14 @@ export interface AgentState {
|
|
|
139
148
|
latestAssistantOutput?: unknown;
|
|
140
149
|
sourceMetadata?: Record<string, unknown>;
|
|
141
150
|
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
151
|
+
attemptCount: number;
|
|
152
|
+
lastAgentEndEvent?: Record<string, unknown>;
|
|
153
|
+
promptStateHash?: string;
|
|
154
|
+
toolStateHash?: string;
|
|
155
|
+
systemStateSequence?: number;
|
|
156
|
+
activeToolCount?: number;
|
|
157
|
+
systemStateChangeCount: number;
|
|
158
|
+
cacheReadTokens: number;
|
|
159
|
+
cacheWriteTokens: number;
|
|
160
|
+
uncachedInputTokens: number;
|
|
142
161
|
}
|
package/src/utils.ts
CHANGED
|
@@ -334,6 +334,55 @@ export function getProviderPayload(event: Record<string, unknown>): unknown {
|
|
|
334
334
|
return event.request ?? event.payload ?? event.body ?? event.providerPayload ?? event.messages ?? event;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
export type ToolUpdateTransport =
|
|
338
|
+
| "anthropic-native"
|
|
339
|
+
| "openai-additional-tools"
|
|
340
|
+
| "openai-tool-search"
|
|
341
|
+
| "mid-conversation-system"
|
|
342
|
+
| "collapsed-leading-system";
|
|
343
|
+
|
|
344
|
+
/** Classify the provider payload observed by this hook rather than guessing from the model name alone. */
|
|
345
|
+
export function inferToolUpdateTransport(payload: unknown, hasPriorSystemState: boolean): ToolUpdateTransport | undefined {
|
|
346
|
+
let sawToolChangeBlock = false;
|
|
347
|
+
let sawAdditionalTools = false;
|
|
348
|
+
let sawToolSearch = false;
|
|
349
|
+
let sawTopLevelTools = false;
|
|
350
|
+
let sawMidConversationSystem = false;
|
|
351
|
+
let visited = 0;
|
|
352
|
+
|
|
353
|
+
const walk = (value: unknown, key?: string) => {
|
|
354
|
+
if (++visited > 2_000 || value === null || value === undefined) return;
|
|
355
|
+
if (key === "additional_tools") sawAdditionalTools = true;
|
|
356
|
+
if (key === "tools") sawTopLevelTools = true;
|
|
357
|
+
if (key === "tool_search" || key === "tool_search_output") sawToolSearch = true;
|
|
358
|
+
if (typeof value !== "object") return;
|
|
359
|
+
if (Array.isArray(value)) {
|
|
360
|
+
let sawNonSystem = false;
|
|
361
|
+
for (const item of value) {
|
|
362
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
363
|
+
const role = (item as Record<string, unknown>).role;
|
|
364
|
+
if (role === "system" && sawNonSystem) sawMidConversationSystem = true;
|
|
365
|
+
if (role && role !== "system") sawNonSystem = true;
|
|
366
|
+
}
|
|
367
|
+
walk(item);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const record = value as Record<string, unknown>;
|
|
372
|
+
const type = record.type;
|
|
373
|
+
if (type === "tool_addition" || type === "tool_removal") sawToolChangeBlock = true;
|
|
374
|
+
for (const [childKey, child] of Object.entries(record)) walk(child, childKey);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
walk(payload);
|
|
378
|
+
if (sawToolChangeBlock) return "anthropic-native";
|
|
379
|
+
if (sawAdditionalTools) return "openai-additional-tools";
|
|
380
|
+
if (sawToolSearch) return "openai-tool-search";
|
|
381
|
+
if (sawMidConversationSystem) return "mid-conversation-system";
|
|
382
|
+
if (hasPriorSystemState && sawTopLevelTools) return "collapsed-leading-system";
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
337
386
|
export function extractModelParameters(payload: unknown): Record<string, string | number> | undefined {
|
|
338
387
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
339
388
|
return undefined;
|
|
@@ -487,6 +536,32 @@ export function extractUsage(
|
|
|
487
536
|
};
|
|
488
537
|
}
|
|
489
538
|
|
|
539
|
+
export function extractCacheMetrics(messageOrEvent: Record<string, unknown>): {
|
|
540
|
+
cacheReadTokens: number;
|
|
541
|
+
cacheWriteTokens: number;
|
|
542
|
+
uncachedInputTokens: number;
|
|
543
|
+
cacheHitRatio?: number;
|
|
544
|
+
} | undefined {
|
|
545
|
+
const usage = (messageOrEvent.usage ??
|
|
546
|
+
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
547
|
+
? (messageOrEvent.message as Record<string, unknown>).usage
|
|
548
|
+
: undefined)) as Record<string, unknown> | undefined;
|
|
549
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
550
|
+
|
|
551
|
+
const uncachedInputTokens = Number(
|
|
552
|
+
usage.input ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens ?? 0,
|
|
553
|
+
);
|
|
554
|
+
const cacheReadTokens = Number(usage.cacheRead ?? usage.cache_read ?? usage.cachedTokens ?? 0);
|
|
555
|
+
const cacheWriteTokens = Number(usage.cacheWrite ?? usage.cache_write ?? 0);
|
|
556
|
+
const denominator = uncachedInputTokens + cacheReadTokens;
|
|
557
|
+
return {
|
|
558
|
+
cacheReadTokens,
|
|
559
|
+
cacheWriteTokens,
|
|
560
|
+
uncachedInputTokens,
|
|
561
|
+
...(denominator > 0 ? { cacheHitRatio: cacheReadTokens / denominator } : {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
490
565
|
export function extractCostDetails(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
|
|
491
566
|
const usage = (messageOrEvent.usage ??
|
|
492
567
|
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
declare module "@opentelemetry/sdk-trace-base" {
|
|
2
|
-
export class BasicTracerProvider {
|
|
3
|
-
constructor(options?: { resource?: unknown; 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
|
-
}
|