pi-langfuse 1.4.3 → 1.4.5

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.
@@ -0,0 +1,261 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ import { CONFIG_PATH } from "./constants.js";
4
+ import { loadConfig, saveConfig, ensureConfig } from "./config.js";
5
+ import { createCapturePolicy, type PrivacyPreset, type CapturePolicy } from "./capture-policy.js";
6
+ import { getRuntime, forceShutdownRuntime as shutdownLangfuseRuntime } from "./langfuse.js";
7
+ import { state } from "./state.js";
8
+ import type { LangfuseRuntime } from "./types.js";
9
+
10
+ const PRIVACY_PRESETS = ["metadata-only", "prompts-only", "conversations", "full-debug"] as const;
11
+
12
+ export interface CommandContextLike {
13
+ hasUI?: boolean;
14
+ ui?: {
15
+ notify?: (message: string, level?: "info" | "warning" | "error") => void;
16
+ };
17
+ }
18
+
19
+ interface CommandDeps {
20
+ configPath?: string;
21
+ getRuntime?: () => Promise<LangfuseRuntime>;
22
+ forceShutdownRuntime?: () => Promise<void>;
23
+ }
24
+
25
+ function notify(ctx: CommandContextLike, message: string, level: "info" | "warning" | "error" = "info") {
26
+ if (ctx.hasUI && ctx.ui?.notify) {
27
+ ctx.ui.notify(message, level);
28
+ return;
29
+ }
30
+ const prefix = level === "error" ? "❌" : level === "warning" ? "⚠️" : "📊";
31
+ console.log(`${prefix} Langfuse: ${message}`);
32
+ }
33
+
34
+ function parseCommandArgs(args: string): { values: Record<string, string>; positional: string[]; malformed: string[] } {
35
+ const values: Record<string, string> = {};
36
+ const positional: string[] = [];
37
+ const malformed: string[] = [];
38
+
39
+ for (const part of args.trim().split(/\s+/)) {
40
+ if (!part) {
41
+ continue;
42
+ }
43
+ const eq = part.indexOf("=");
44
+ if (eq === -1) {
45
+ positional.push(part);
46
+ continue;
47
+ }
48
+ if (eq === 0) {
49
+ malformed.push(part);
50
+ continue;
51
+ }
52
+ values[part.slice(0, eq)] = part.slice(eq + 1);
53
+ }
54
+
55
+ return { values, positional, malformed };
56
+ }
57
+
58
+ function isPrivacyPreset(value: string | undefined): value is PrivacyPreset {
59
+ return PRIVACY_PRESETS.includes(value as PrivacyPreset);
60
+ }
61
+
62
+ function inferPreset(policy: CapturePolicy): PrivacyPreset | "custom" {
63
+ const entries: Array<[PrivacyPreset, CapturePolicy]> = [
64
+ [
65
+ "metadata-only",
66
+ {
67
+ captureInputs: false,
68
+ captureOutputs: false,
69
+ captureToolIo: false,
70
+ captureSystemPrompt: false,
71
+ captureCwd: false,
72
+ },
73
+ ],
74
+ [
75
+ "prompts-only",
76
+ {
77
+ captureInputs: true,
78
+ captureOutputs: false,
79
+ captureToolIo: false,
80
+ captureSystemPrompt: false,
81
+ captureCwd: false,
82
+ },
83
+ ],
84
+ [
85
+ "conversations",
86
+ {
87
+ captureInputs: true,
88
+ captureOutputs: true,
89
+ captureToolIo: false,
90
+ captureSystemPrompt: false,
91
+ captureCwd: false,
92
+ },
93
+ ],
94
+ [
95
+ "full-debug",
96
+ {
97
+ captureInputs: true,
98
+ captureOutputs: true,
99
+ captureToolIo: true,
100
+ captureSystemPrompt: true,
101
+ captureCwd: true,
102
+ },
103
+ ],
104
+ ];
105
+
106
+ for (const [preset, presetPolicy] of entries) {
107
+ if (
108
+ policy.captureInputs === presetPolicy.captureInputs &&
109
+ policy.captureOutputs === presetPolicy.captureOutputs &&
110
+ policy.captureToolIo === presetPolicy.captureToolIo &&
111
+ policy.captureSystemPrompt === presetPolicy.captureSystemPrompt &&
112
+ policy.captureCwd === presetPolicy.captureCwd
113
+ ) {
114
+ return preset;
115
+ }
116
+ }
117
+ return "custom";
118
+ }
119
+
120
+ function describePolicy(policy: CapturePolicy) {
121
+ return [
122
+ `captureInputs: ${policy.captureInputs}`,
123
+ `captureOutputs: ${policy.captureOutputs}`,
124
+ `captureToolIo: ${policy.captureToolIo}`,
125
+ `captureSystemPrompt: ${policy.captureSystemPrompt}`,
126
+ `captureCwd: ${policy.captureCwd}`,
127
+ ].join("\n");
128
+ }
129
+
130
+ function readPersistedConfig(path: string) {
131
+ if (!existsSync(path)) {
132
+ return {};
133
+ }
134
+ try {
135
+ return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
136
+ } catch {
137
+ return {};
138
+ }
139
+ }
140
+
141
+ function hasActiveAgentObservation() {
142
+ for (const sessionState of state.sessionStates.values()) {
143
+ if (sessionState.agentState?.root) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
150
+ export async function handleLangfusePrivacyCommand(
151
+ args: string,
152
+ ctx: CommandContextLike,
153
+ deps: CommandDeps = {},
154
+ ): Promise<boolean> {
155
+ const configPath = deps.configPath ?? CONFIG_PATH;
156
+ const parsed = parseCommandArgs(args);
157
+ if (parsed.malformed.length > 0) {
158
+ notify(ctx, `Couldn't understand '${parsed.malformed[0]}'. Use /langfuse-privacy preset=metadata-only.`, "warning");
159
+ return false;
160
+ }
161
+
162
+ const requestedPreset = parsed.values.preset ?? parsed.positional[0];
163
+ if (!requestedPreset) {
164
+ state.config = state.config ?? loadConfig(process.env, configPath);
165
+ const policy = state.config?.capturePolicy ?? createCapturePolicy();
166
+ notify(ctx, `Current Langfuse privacy preset: ${inferPreset(policy)}\n${describePolicy(policy)}`);
167
+ return true;
168
+ }
169
+
170
+ if (!isPrivacyPreset(requestedPreset)) {
171
+ notify(
172
+ ctx,
173
+ `Unknown privacy preset '${requestedPreset}'. Use one of: ${PRIVACY_PRESETS.join(", ")}.`,
174
+ "warning",
175
+ );
176
+ return false;
177
+ }
178
+
179
+ const existing = readPersistedConfig(configPath);
180
+ const loaded = state.config ?? loadConfig(process.env, configPath);
181
+
182
+ const publicKey = existing.publicKey ?? loaded?.publicKey;
183
+ const secretKey = existing.secretKey ?? loaded?.secretKey;
184
+ const host = existing.host ?? loaded?.host;
185
+
186
+ if (!publicKey || !secretKey || !host) {
187
+ notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup before changing privacy settings.", "warning");
188
+ return false;
189
+ }
190
+
191
+ const nextConfig = {
192
+ publicKey: String(publicKey),
193
+ secretKey: String(secretKey),
194
+ host: String(host),
195
+ privacyPreset: requestedPreset,
196
+ };
197
+ saveConfig(nextConfig, configPath);
198
+ state.config = loadConfig(process.env, configPath);
199
+
200
+ notify(ctx, `Langfuse privacy preset saved: ${requestedPreset}\n${describePolicy(state.config?.capturePolicy ?? createCapturePolicy())}`);
201
+ return true;
202
+ }
203
+
204
+ export async function handleLangfuseTestCommand(
205
+ _args: string,
206
+ ctx: CommandContextLike,
207
+ deps: CommandDeps = {},
208
+ ): Promise<boolean> {
209
+ if (!state.config && !(await ensureConfig(ctx))) {
210
+ notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup first.", "warning");
211
+ return false;
212
+ }
213
+
214
+ if (hasActiveAgentObservation()) {
215
+ notify(ctx, "Langfuse test skipped because an agent run is active. Try again after the run finishes.", "warning");
216
+ return false;
217
+ }
218
+
219
+ let runtimeInitialized = false;
220
+ try {
221
+ const rt = await (deps.getRuntime ?? getRuntime)();
222
+ runtimeInitialized = true;
223
+ rt.propagateAttributes(
224
+ {
225
+ traceName: "pi-langfuse-test",
226
+ metadata: {
227
+ source: "pi-langfuse",
228
+ command: "langfuse-test",
229
+ },
230
+ },
231
+ () => {
232
+ const observation = rt.startObservation(
233
+ "pi-langfuse-test",
234
+ {
235
+ input: { command: "/langfuse-test" },
236
+ output: "ok",
237
+ metadata: {
238
+ source: "pi-langfuse",
239
+ command: "langfuse-test",
240
+ },
241
+ },
242
+ { asType: "span" },
243
+ );
244
+ observation.end();
245
+ return observation;
246
+ },
247
+ );
248
+ await rt.tracerProvider?.forceFlush?.();
249
+ await rt.scoreClient.flush?.();
250
+ notify(ctx, `Langfuse test succeeded. Test trace sent to ${state.config?.host}.`);
251
+ return true;
252
+ } catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ notify(ctx, `Langfuse test failed: ${message}`, "error");
255
+ return false;
256
+ } finally {
257
+ if (runtimeInitialized) {
258
+ await (deps.forceShutdownRuntime ?? shutdownLangfuseRuntime)();
259
+ }
260
+ }
261
+ }
package/src/config.ts CHANGED
@@ -1,19 +1,27 @@
1
1
  import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import type { Config } from "./types.js";
3
- import { CONFIG_DIR, CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
4
+ import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
4
5
  import { state } from "./state.js";
5
- import { shutdownRuntime } from "./langfuse.js";
6
+ import { forceShutdownRuntime } from "./langfuse.js";
7
+ import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
6
8
 
7
- export function loadConfigFromFile(): Config | null {
8
- if (existsSync(CONFIG_PATH)) {
9
+ export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
10
+ if (existsSync(path)) {
9
11
  try {
10
- const content = readFileSync(CONFIG_PATH, "utf-8");
11
- const config = JSON.parse(content) as Config;
12
+ const content = readFileSync(path, "utf-8");
13
+ const config = JSON.parse(content) as Config & { capture?: EnvLike; privacyPreset?: string };
12
14
  if (config.publicKey && config.secretKey) {
15
+ const captureSource: EnvLike = {
16
+ ...(config.capture ?? {}),
17
+ ...(config.privacyPreset ? { LANGFUSE_PRIVACY_PRESET: config.privacyPreset } : {}),
18
+ ...env,
19
+ };
13
20
  return {
14
21
  publicKey: config.publicKey,
15
22
  secretKey: config.secretKey,
16
23
  host: config.host || DEFAULT_LANGFUSE_HOST,
24
+ capturePolicy: createCapturePolicy(captureSource),
17
25
  };
18
26
  }
19
27
  } catch (e) {
@@ -24,9 +32,9 @@ export function loadConfigFromFile(): Config | null {
24
32
  return null;
25
33
  }
26
34
 
27
- export function loadConfigFromEnv(): Config | null {
28
- const publicKey = process.env.LANGFUSE_PUBLIC_KEY || "";
29
- const secretKey = process.env.LANGFUSE_SECRET_KEY || "";
35
+ export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config | null {
36
+ const publicKey = env.LANGFUSE_PUBLIC_KEY || "";
37
+ const secretKey = env.LANGFUSE_SECRET_KEY || "";
30
38
  if (!publicKey || !secretKey) {
31
39
  return null;
32
40
  }
@@ -34,17 +42,18 @@ export function loadConfigFromEnv(): Config | null {
34
42
  return {
35
43
  publicKey,
36
44
  secretKey,
37
- host: process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
45
+ host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
46
+ capturePolicy: createCapturePolicy(env),
38
47
  };
39
48
  }
40
49
 
41
- export function loadConfig(): Config | null {
42
- return loadConfigFromFile() || loadConfigFromEnv();
50
+ export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_PATH): Config | null {
51
+ return loadConfigFromFile(path, env) || loadConfigFromEnv(env);
43
52
  }
44
53
 
45
- export function saveConfig(config: Config) {
46
- mkdirSync(CONFIG_DIR, { recursive: true });
47
- writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
54
+ export function saveConfig(config: Config, path = CONFIG_PATH) {
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
48
57
  }
49
58
 
50
59
  async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | null> {
@@ -72,6 +81,7 @@ async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | n
72
81
  publicKey,
73
82
  secretKey,
74
83
  host: hostInput || DEFAULT_LANGFUSE_HOST,
84
+ capturePolicy: createCapturePolicy(),
75
85
  };
76
86
  }
77
87
 
@@ -115,7 +125,7 @@ export async function ensureConfig(ctx: any): Promise<boolean> {
115
125
  export async function promptForConfig(ctx: any): Promise<boolean> {
116
126
  state.setupAttemptedThisSession = false;
117
127
  state.config = null;
118
- await shutdownRuntime();
128
+ await forceShutdownRuntime();
119
129
 
120
130
  const config = await collectConfigFromUI(ctx, "Manual setup requested");
121
131
  if (!config) {
@@ -1,8 +1,25 @@
1
1
  import { state, resetRunState, computeEvaluationScores } from "../state.js";
2
2
  import { getRuntime, sendScore } from "../langfuse.js";
3
3
  import { ensureConfig } from "../config.js";
4
- import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput } from "../utils.js";
4
+ import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
5
5
  import { closeDanglingObservations } from "./tool.js";
6
+ import { applyCapturePolicy } from "../capture-policy.js";
7
+
8
+ function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
9
+ if (!metadata) {
10
+ return undefined;
11
+ }
12
+
13
+ const output: Record<string, string> = {};
14
+ for (const [key, value] of Object.entries(metadata)) {
15
+ if (typeof value === "string") {
16
+ output[key] = value;
17
+ } else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
18
+ output[key] = String(value);
19
+ }
20
+ }
21
+ return Object.keys(output).length > 0 ? output : undefined;
22
+ }
6
23
 
7
24
  export function updateTraceIO(input?: unknown, output?: unknown) {
8
25
  const root = state.agentState?.root;
@@ -45,15 +62,28 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
45
62
  // Ignore if getSystemPrompt is not available or fails
46
63
  }
47
64
 
48
- const promptInput = shapePayload({
65
+ const rawPromptInput = shapePayload({
49
66
  prompt: event.prompt,
50
67
  images: event.images,
51
68
  context: event.context ?? event.attachments,
52
69
  });
70
+ const captured = applyCapturePolicy(
71
+ {
72
+ input: rawPromptInput,
73
+ metadata: {
74
+ cwd,
75
+ ...(state.currentModel ? { model: state.currentModel } : {}),
76
+ ...(state.currentProvider ? { provider: state.currentProvider } : {}),
77
+ sessionId: state.currentSessionId || undefined,
78
+ },
79
+ systemPrompt: systemPrompt ? truncate(String(systemPrompt), 20000) : undefined,
80
+ },
81
+ getCapturePolicy(),
82
+ );
53
83
 
54
84
  state.agentState = {
55
85
  cwd,
56
- promptInput,
86
+ promptInput: captured.input,
57
87
  generationSeq: 0,
58
88
  activeGenerations: new Map(),
59
89
  generationOrder: [],
@@ -65,32 +95,25 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
65
95
  {
66
96
  sessionId: state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined,
67
97
  traceName: "pi-agent",
68
- metadata: {
69
- cwd: truncate(cwd, 200),
70
- ...(state.currentModel ? { model: truncate(state.currentModel, 200) } : {}),
71
- ...(state.currentProvider ? { provider: truncate(state.currentProvider, 200) } : {}),
72
- },
98
+ metadata: stringMetadata(captured.metadata),
73
99
  },
74
100
  () =>
75
101
  rt.startObservation(
76
102
  "pi-agent",
77
- {
78
- input: promptInput,
79
- metadata: {
80
- cwd,
81
- model: state.currentModel || undefined,
82
- provider: state.currentProvider || undefined,
83
- sessionId: state.currentSessionId || undefined,
84
- ...(systemPrompt ? { systemPrompt: truncate(String(systemPrompt), 20000) } : {}),
103
+ {
104
+ input: captured.input,
105
+ metadata: {
106
+ ...(captured.metadata ?? {}),
107
+ ...(captured.systemPrompt ? { systemPrompt: captured.systemPrompt } : {}),
108
+ },
85
109
  },
86
- },
87
110
  { asType: "agent" },
88
111
  ),
89
112
  );
90
113
 
91
114
  state.agentState.root = root;
92
115
  state.agentState.traceId = root.traceId;
93
- updateTraceIO(promptInput, undefined);
116
+ updateTraceIO(captured.input, undefined);
94
117
  } catch (e) {
95
118
  console.warn("📊 Langfuse: Failed to create agent observation", e);
96
119
  state.isTracingDisabled = true;
@@ -104,7 +127,21 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
104
127
  }
105
128
 
106
129
  const lastAssistant = extractFinalAssistant(event.messages);
107
- const output = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
130
+ const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
131
+ const captured = applyCapturePolicy(
132
+ {
133
+ output: rawOutput,
134
+ metadata: {
135
+ cwd: state.agentState.cwd,
136
+ completed: true,
137
+ model: state.currentModel || undefined,
138
+ provider: state.currentProvider || undefined,
139
+ totalTools: state.toolCallCount,
140
+ ...computeEvaluationScores(),
141
+ },
142
+ },
143
+ getCapturePolicy(),
144
+ );
108
145
  const scores = computeEvaluationScores();
109
146
 
110
147
  closeDanglingObservations("Agent run ended before observation finalized");
@@ -112,18 +149,11 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
112
149
  try {
113
150
  state.agentState.root
114
151
  .update({
115
- output,
116
- metadata: {
117
- cwd: state.agentState.cwd,
118
- completed: true,
119
- model: state.currentModel || undefined,
120
- provider: state.currentProvider || undefined,
121
- totalTools: state.toolCallCount,
122
- ...scores,
123
- },
152
+ output: captured.output,
153
+ metadata: captured.metadata,
124
154
  })
125
155
  .end();
126
- updateTraceIO(state.agentState.promptInput, output);
156
+ updateTraceIO(state.agentState.promptInput, captured.output);
127
157
 
128
158
  await sendScore("tool_call_count", scores.tool_call_count, { traceId: state.agentState.traceId });
129
159
  await sendScore("turn_count", scores.turn_count, { traceId: state.agentState.traceId });
@@ -1,5 +1,6 @@
1
1
  import { state } from "../state.js";
2
2
  import { getRuntime } from "../langfuse.js";
3
+ import { startChildObservation } from "../observation.js";
3
4
  import {
4
5
  getRequestKey,
5
6
  getProviderPayload,
@@ -9,8 +10,10 @@ import {
9
10
  extractAssistantOutput,
10
11
  extractUsage,
11
12
  extractCostDetails,
13
+ getCapturePolicy,
12
14
  } from "../utils.js";
13
15
  import type { GenerationState, ObservationUpdate } from "../types.js";
16
+ import { applyCapturePolicy } from "../capture-policy.js";
14
17
 
15
18
  export function getOpenGeneration(): GenerationState | undefined {
16
19
  if (state.isTracingDisabled || !state.agentState) {
@@ -44,33 +47,32 @@ export async function startGeneration(event: Record<string, unknown>) {
44
47
  url: event.url,
45
48
  method: event.method,
46
49
  }) as Record<string, unknown>;
50
+ const captured = applyCapturePolicy(
51
+ {
52
+ input: shapePayload(payload),
53
+ metadata,
54
+ },
55
+ getCapturePolicy(),
56
+ );
47
57
 
48
58
  const parent = state.agentState.activeTurn ?? state.agentState.root;
49
- const generation = parent.startObservation
50
- ? parent.startObservation(
51
- "llm-generation",
52
- {
53
- input: shapePayload(payload),
54
- model: model || undefined,
55
- metadata,
56
- },
57
- { asType: "generation" },
58
- )
59
- : (await getRuntime()).startObservation(
60
- "llm-generation",
61
- {
62
- input: shapePayload(payload),
63
- model: model || undefined,
64
- metadata,
65
- },
66
- { asType: "generation" },
67
- );
59
+ const generation = await startChildObservation({
60
+ parent,
61
+ runtime: getRuntime,
62
+ name: "llm-generation",
63
+ body: {
64
+ input: captured.input,
65
+ model: model || undefined,
66
+ metadata: captured.metadata,
67
+ },
68
+ asType: "generation",
69
+ });
68
70
 
69
71
  state.agentState.activeGenerations.set(key, {
70
72
  observation: generation,
71
73
  requestKey: key,
72
74
  ended: false,
73
- metadata,
75
+ metadata: captured.metadata ?? {},
74
76
  });
75
77
  state.agentState.generationOrder.push(key);
76
78
  } catch (e) {
@@ -84,7 +86,7 @@ export function updateGenerationMetadata(event: Record<string, unknown>) {
84
86
  }
85
87
 
86
88
  const key = getRequestKey(event, "");
87
- const metadata = extractResponseMetadata(event);
89
+ const metadata = applyCapturePolicy({ metadata: extractResponseMetadata(event) }, getCapturePolicy()).metadata ?? {};
88
90
  if (!key) {
89
91
  const generation = getOpenGeneration();
90
92
  if (generation) {
@@ -160,7 +162,9 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
160
162
  }
161
163
 
162
164
  const generation = getOpenGeneration();
163
- const output = extractAssistantOutput(message);
165
+ const rawOutput = extractAssistantOutput(message);
166
+ const captured = applyCapturePolicy({ output: rawOutput }, getCapturePolicy());
167
+ const output = captured.output;
164
168
  state.agentState.latestAssistantOutput = output;
165
169
 
166
170
  if (!generation) {
@@ -180,6 +184,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
180
184
  finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
181
185
  },
182
186
  };
187
+ update.metadata = applyCapturePolicy({ metadata: update.metadata }, getCapturePolicy()).metadata;
183
188
 
184
189
  try {
185
190
  generation.observation.update(update).end();
@@ -198,38 +203,32 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
198
203
  const usageDetails = extractUsage({ ...event, message });
199
204
  const costDetails = extractCostDetails({ ...event, message });
200
205
  const model = String(message.model ?? event.model ?? state.currentModel ?? "");
206
+ const captured = applyCapturePolicy(
207
+ {
208
+ input: state.agentState.promptInput,
209
+ output: extractAssistantOutput(message),
210
+ metadata: {
211
+ provider: state.currentProvider || undefined,
212
+ sourceEvent: "turn_end",
213
+ },
214
+ },
215
+ getCapturePolicy(),
216
+ );
201
217
  const parent = state.agentState.activeTurn ?? state.agentState.root;
202
- const generation = parent.startObservation
203
- ? parent.startObservation(
204
- "llm-generation",
205
- {
206
- input: state.agentState.promptInput,
207
- output: extractAssistantOutput(message),
208
- model: model || undefined,
209
- usageDetails,
210
- costDetails,
211
- metadata: {
212
- provider: state.currentProvider || undefined,
213
- sourceEvent: "turn_end",
214
- },
215
- },
216
- { asType: "generation" },
217
- )
218
- : (await getRuntime()).startObservation(
219
- "llm-generation",
220
- {
221
- input: state.agentState.promptInput,
222
- output: extractAssistantOutput(message),
223
- model: model || undefined,
224
- usageDetails,
225
- costDetails,
226
- metadata: {
227
- provider: state.currentProvider || undefined,
228
- sourceEvent: "turn_end",
229
- },
230
- },
231
- { asType: "generation" },
232
- );
218
+ const generation = await startChildObservation({
219
+ parent,
220
+ runtime: getRuntime,
221
+ name: "llm-generation",
222
+ body: {
223
+ input: captured.input,
224
+ output: captured.output,
225
+ model: model || undefined,
226
+ usageDetails,
227
+ costDetails,
228
+ metadata: captured.metadata,
229
+ },
230
+ asType: "generation",
231
+ });
233
232
 
234
233
  generation.end();
235
234
  state.agentState.generationOrder.push("turn-end-fallback");