@narumitw/pi-langfuse 0.50.6 → 0.51.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
@@ -11,7 +11,8 @@ Export Pi agent runs, model generations, retries, tools, compaction, usage, and
11
11
  - Captures final tool inputs and outputs, progress timing, duration, failures, and structural compaction events.
12
12
  - Adds session, context, Git branch, commit, and aggregate counters as searchable metadata.
13
13
  - Supports metadata-only tracing when content capture is disabled.
14
- - Reads credentials only from a private local settings file and redacts them from exported data.
14
+ - Keeps the default extension's credentials in a private local settings file and redacts them from exported data.
15
+ - Supports multiple concurrent Pi sessions through one process-owned runtime without sharing session trace state.
15
16
  - Batches exports without delaying normal completion and isolates its OpenTelemetry provider from other extensions.
16
17
 
17
18
  ## 📦 Install
@@ -43,6 +44,76 @@ Review this extension and its data-export behavior before installing it.
43
44
  Run `/langfuse`, choose **Set up Langfuse for this Pi agent directory**, enter your credentials, and restart Pi.
44
45
  New agent runs are then traced with the saved content-capture and metadata settings.
45
46
 
47
+ ## 🧩 Host application embedding
48
+
49
+ A host that manages multiple Pi sessions can share one process-level runtime and create one tracing controller for each session:
50
+
51
+ ```ts
52
+ import { DefaultResourceLoader, createAgentSession } from "@earendil-works/pi-coding-agent";
53
+ import {
54
+ createLangfuseRuntime,
55
+ createPiLangfuseSession,
56
+ } from "@narumitw/pi-langfuse";
57
+
58
+ const runtime = await createLangfuseRuntime({
59
+ config: { environment: "production" },
60
+ // publicKey, secretKey, baseUrl, and release may come from standard Langfuse env vars.
61
+ });
62
+
63
+ const tracing = createPiLangfuseSession(runtime, {
64
+ traceName: "support-agent",
65
+ sessionId: hostSessionId,
66
+ userId: accountId,
67
+ tags: ["support"],
68
+ metadata: { tenant: tenantId },
69
+ captureContent: false,
70
+ onTraceId: (traceId) => correlateTrace(hostSessionId, traceId),
71
+ });
72
+
73
+ const resourceLoader = new DefaultResourceLoader({
74
+ ...hostResourceOptions,
75
+ noExtensions: true, // Keep auto-discovered extensions out of this controlled host session.
76
+ extensionFactories: [{ name: "langfuse", factory: tracing.extension }],
77
+ });
78
+ await resourceLoader.reload();
79
+ const { session } = await createAgentSession({
80
+ ...hostSessionOptions,
81
+ resourceLoader,
82
+ });
83
+ await session.bindExtensions({});
84
+
85
+ tracing.setRequestId(incomingRequestId); // Applies to the next root trace only.
86
+ await session.prompt(userPrompt);
87
+
88
+ // When this session ends:
89
+ await tracing.dispose();
90
+ session.dispose();
91
+
92
+ // Once, after every hosted session has been disposed:
93
+ await runtime.shutdown();
94
+ ```
95
+
96
+ Create the runtime once per process, create a separate controller per Pi `AgentSession`, and explicitly dispose the controller because `AgentSession.dispose()` does not emit Pi's `session_shutdown` event.
97
+ The controller's extension factory may be reused when that session's resource loader reloads, but it must not be attached to another active `AgentSession`.
98
+ Controller disposal ends only that session's open observations; it never flushes or shuts down the shared exporter.
99
+ `runtime.shutdown()` rejects new work, disposes any remaining controllers, flushes, and shuts down the provider once.
100
+ Both operations are idempotent.
101
+
102
+ Do not also auto-load this package's default extension into a session that uses `tracing.extension`, or that session will be traced twice.
103
+ One process supports one active Langfuse credential/endpoint combination; creating a conflicting runtime fails and requires a process restart rather than replacing the active provider.
104
+ Restart the process before switching from a package version that uses an older runtime protocol so two providers cannot initialize concurrently.
105
+
106
+ Host runtime fields resolve independently in this order: `config`, the supplied `env` object (or `process.env`), then defaults.
107
+ The supported names are `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_BASE_URL`, `LANGFUSE_TRACING_ENVIRONMENT`, and `LANGFUSE_RELEASE`.
108
+ Pass `env: false` to disable environment access.
109
+ The default extension always uses its validated `pi-langfuse.json` with `env: false`, so its existing settings behavior does not change.
110
+
111
+ `setRequestId()` snapshots `pi.request.id` when the next root trace starts; changing or clearing it never rewrites an active trace.
112
+ `traceName` changes only the Langfuse trace name; schema-v2 observation names and hierarchy stay unchanged.
113
+ `onTraceId` runs once after each root is created, including automatic-continuation roots, and callback errors are intentionally ignored so host code cannot corrupt tracing cleanup.
114
+ Custom metadata is sanitized, bounded, and exported even when `captureContent` is `false`; package-owned `pi.*` fields cannot be overridden.
115
+ Treat custom metadata, tags, session IDs, user IDs, request IDs, and trace names as exported data.
116
+
46
117
  ## ⚙️ Settings
47
118
 
48
119
  Run the interactive manager and choose **Set up Langfuse for this Pi agent directory** or **Update Langfuse for this Pi agent directory**:
@@ -156,6 +227,9 @@ The request snapshot is the payload visible to this handler, not a guaranteed fi
156
227
  A later extension can still replace it.
157
228
  Final assistant content is reconciled from `turn_end` and `agent_end` after message transformation.
158
229
  A recovered sequence such as `429 -> 200` remains queryable in HTTP metadata but is not an error; the final assistant outcome decides generation severity.
230
+ A provider request becomes a `pi.llm` generation only when Pi emits an assistant lifecycle event.
231
+ Cache-warming requests emit provider hooks without that lifecycle, so they are excluded instead of becoming orphaned or interrupted ordinary generations.
232
+ Dedicated cache-warm observations are deferred until Pi exposes a deterministic request-kind and completion signal.
159
233
 
160
234
  ### Tool and compaction fields
161
235
 
@@ -224,9 +298,13 @@ Compaction summaries, tool partial results, opaque continuation signatures, auth
224
298
  ```text
225
299
  packages/pi-langfuse/
226
300
  ├── src/ # Authoritative implementation and helpers
227
- │ ├── index.ts # Thin Pi entrypoint
228
- │ └── langfuse.ts # Tracing lifecycle and command
229
- ├── dist/ # Generated Jiti runtime
301
+ │ ├── index.ts # Thin default and public entrypoint
302
+ │ ├── langfuse.ts # Default settings and command adapter
303
+ │ ├── pi-session.ts # Per-session Pi lifecycle controller
304
+ │ ├── runtime-core.ts # Process-runtime ownership
305
+ │ ├── runtime.ts # Lazy production Langfuse runtime
306
+ │ └── tracing.ts # Schema-v2 observation recorder
307
+ ├── dist/ # Generated Node, declaration, and Jiti runtime
230
308
  ├── scripts/build-runtime.mjs # Runtime builder
231
309
  └── test/ # Behavior and lifecycle coverage
232
310
  ```
@@ -0,0 +1,316 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/config.ts
5
+ import { randomUUID } from "node:crypto";
6
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
9
+ var CONFIG_FILE_NAME = "pi-langfuse.json";
10
+ var DEFAULT_BASE_URL = "https://us.cloud.langfuse.com";
11
+ function langfuseConfigPath() {
12
+ return join(getAgentDir(), CONFIG_FILE_NAME);
13
+ }
14
+ var configSaveQueues = /* @__PURE__ */ new Map();
15
+ function writeLangfuseConfig(patch, path = langfuseConfigPath()) {
16
+ const previous = configSaveQueues.get(path) ?? Promise.resolve();
17
+ const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
18
+ const tail = operation.then(
19
+ () => void 0,
20
+ () => void 0
21
+ );
22
+ configSaveQueues.set(path, tail);
23
+ void tail.then(() => {
24
+ if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
25
+ });
26
+ return operation;
27
+ }
28
+ async function writeLangfuseConfigNow(patch, path) {
29
+ const currentDocument = await readLangfuseDocumentForUpdate(path);
30
+ const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
31
+ if (!normalized.ok) throw new Error(normalized.reason);
32
+ const {
33
+ publicKey: _publicKey,
34
+ secretKey: _secretKey,
35
+ baseUrl: _baseUrl,
36
+ environment: _environment,
37
+ release: _release,
38
+ userId: _userId,
39
+ captureContent: _captureContent,
40
+ ...unknownFields
41
+ } = currentDocument;
42
+ const nextDocument = { ...unknownFields, ...normalized.config };
43
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
44
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
45
+ try {
46
+ await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}
47
+ `, {
48
+ encoding: "utf8",
49
+ mode: 384
50
+ });
51
+ await chmod(tempPath, 384);
52
+ await rename(tempPath, path);
53
+ return normalized.config;
54
+ } catch (error) {
55
+ await rm(tempPath, { force: true }).catch(() => void 0);
56
+ throw error;
57
+ }
58
+ }
59
+ async function readLangfuseDocumentForUpdate(path) {
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(await readFile(path, "utf8"));
63
+ } catch (error) {
64
+ if (isNodeError(error) && error.code === "ENOENT") return {};
65
+ throw new Error(
66
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`
67
+ );
68
+ }
69
+ const normalized = normalizeLangfuseConfig(parsed);
70
+ if (!normalized.ok) {
71
+ throw new Error(
72
+ `Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`
73
+ );
74
+ }
75
+ return { ...parsed };
76
+ }
77
+ async function loadLangfuseConfig(path = langfuseConfigPath()) {
78
+ await configSaveQueues.get(path);
79
+ const warnings = [];
80
+ const permissionFailure = await ensurePrivatePermissions(path, warnings);
81
+ if (permissionFailure) {
82
+ return {
83
+ ok: false,
84
+ path,
85
+ warnings,
86
+ reason: `Refusing to load credentials: ${permissionFailure}`
87
+ };
88
+ }
89
+ let text;
90
+ try {
91
+ text = await readFile(path, "utf8");
92
+ } catch (error) {
93
+ if (isNodeError(error) && error.code === "ENOENT") {
94
+ return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
95
+ }
96
+ return {
97
+ ok: false,
98
+ path,
99
+ warnings,
100
+ reason: `Failed to read ${path}: ${formatError(error)}`
101
+ };
102
+ }
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(text);
106
+ } catch {
107
+ return {
108
+ ok: false,
109
+ path,
110
+ warnings,
111
+ reason: `Failed to parse ${path} as JSON; repair the file before retrying.`
112
+ };
113
+ }
114
+ const normalized = normalizeLangfuseConfig(parsed);
115
+ if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
116
+ return { ok: true, config: normalized.config, path, warnings };
117
+ }
118
+ function normalizeLangfuseConfig(value) {
119
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
120
+ return { ok: false, reason: "pi-langfuse.json must contain a JSON object." };
121
+ }
122
+ const input = value;
123
+ const publicKey = normalizeString(input.publicKey);
124
+ const secretKey = normalizeString(input.secretKey);
125
+ if (!publicKey) {
126
+ return { ok: false, reason: "pi-langfuse.json requires a literal publicKey string." };
127
+ }
128
+ if (!secretKey) {
129
+ return { ok: false, reason: "pi-langfuse.json requires a literal secretKey string." };
130
+ }
131
+ if (isInterpolation(publicKey)) {
132
+ return {
133
+ ok: false,
134
+ reason: "pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported."
135
+ };
136
+ }
137
+ if (isInterpolation(secretKey)) {
138
+ return {
139
+ ok: false,
140
+ reason: "pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported."
141
+ };
142
+ }
143
+ const rawBaseUrl = input.baseUrl === void 0 ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);
144
+ if (!rawBaseUrl) {
145
+ return { ok: false, reason: "pi-langfuse.json baseUrl must be a non-empty string." };
146
+ }
147
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
148
+ if (!baseUrl) {
149
+ return {
150
+ ok: false,
151
+ reason: "pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment."
152
+ };
153
+ }
154
+ if (input.captureContent !== void 0 && typeof input.captureContent !== "boolean") {
155
+ return { ok: false, reason: "pi-langfuse.json captureContent must be a boolean." };
156
+ }
157
+ const environment = optionalString(input.environment, "environment");
158
+ if (!environment.ok) return environment;
159
+ if (environment.value && (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))) {
160
+ return {
161
+ ok: false,
162
+ reason: "pi-langfuse.json environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse."
163
+ };
164
+ }
165
+ const release = optionalString(input.release, "release");
166
+ if (!release.ok) return release;
167
+ const userId = optionalString(input.userId, "userId");
168
+ if (!userId.ok) return userId;
169
+ if (userId.value && userId.value.length > 200) {
170
+ return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
171
+ }
172
+ return {
173
+ ok: true,
174
+ config: {
175
+ publicKey,
176
+ secretKey,
177
+ baseUrl,
178
+ ...environment.value ? { environment: environment.value } : {},
179
+ ...release.value ? { release: release.value } : {},
180
+ ...userId.value ? { userId: userId.value } : {},
181
+ captureContent: input.captureContent ?? true
182
+ }
183
+ };
184
+ }
185
+ async function ensurePrivatePermissions(path, warnings) {
186
+ try {
187
+ const file = await stat(path);
188
+ if ((file.mode & 511) !== 384) {
189
+ await chmod(path, 384);
190
+ const repaired = await stat(path);
191
+ if ((repaired.mode & 511) !== 384) {
192
+ throw new Error(`permissions remained ${(repaired.mode & 511).toString(8)}`);
193
+ }
194
+ warnings.push(`Restricted ${path} permissions to 0600.`);
195
+ }
196
+ } catch (error) {
197
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
198
+ const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;
199
+ warnings.push(failure);
200
+ return failure;
201
+ }
202
+ return void 0;
203
+ }
204
+ function normalizeBaseUrl(value) {
205
+ try {
206
+ const url = new URL(value);
207
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
208
+ if (url.username || url.password || url.search || url.hash) return void 0;
209
+ return url.toString().replace(/\/+$/, "");
210
+ } catch {
211
+ return void 0;
212
+ }
213
+ }
214
+ function optionalString(value, name) {
215
+ if (value === void 0) return { ok: true };
216
+ const normalized = normalizeString(value);
217
+ return normalized ? { ok: true, value: normalized } : { ok: false, reason: `pi-langfuse.json ${name} must be a non-empty string.` };
218
+ }
219
+ function normalizeString(value) {
220
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
221
+ }
222
+ function isInterpolation(value) {
223
+ return value.startsWith("$") || value.startsWith("!");
224
+ }
225
+ function isNodeError(error) {
226
+ return error instanceof Error && "code" in error;
227
+ }
228
+ function formatError(error) {
229
+ return error instanceof Error ? error.message : String(error);
230
+ }
231
+
232
+ // src/runtime-core.ts
233
+ var RUNTIME_INTERNAL_KEY = /* @__PURE__ */ Symbol.for("@narumitw/pi-langfuse/runtime-internal/v2");
234
+ var ManagedLangfuseRuntime = class {
235
+ constructor(backend) {
236
+ this.backend = backend;
237
+ }
238
+ backend;
239
+ state = "open";
240
+ sessionDisposers = /* @__PURE__ */ new Set();
241
+ flushQueue = Promise.resolve();
242
+ shutdownPromise;
243
+ get closed() {
244
+ return this.state !== "open";
245
+ }
246
+ registerSession(disposer) {
247
+ if (this.state !== "open") throw new Error("Langfuse runtime is shutting down or already closed.");
248
+ this.sessionDisposers.add(disposer);
249
+ return () => this.sessionDisposers.delete(disposer);
250
+ }
251
+ flush() {
252
+ if (this.state !== "open") {
253
+ return Promise.reject(new Error("Langfuse runtime is shutting down or already closed."));
254
+ }
255
+ const operation = this.flushQueue.then(() => this.backend.forceFlush());
256
+ this.flushQueue = operation.then(
257
+ () => void 0,
258
+ () => void 0
259
+ );
260
+ return operation;
261
+ }
262
+ shutdown() {
263
+ if (this.shutdownPromise) return this.shutdownPromise;
264
+ this.state = "closing";
265
+ this.shutdownPromise = this.shutdownNow();
266
+ return this.shutdownPromise;
267
+ }
268
+ async shutdownNow() {
269
+ const errors = [];
270
+ for (const dispose of [...this.sessionDisposers]) {
271
+ try {
272
+ await dispose("Langfuse runtime shut down before the Pi session was disposed.");
273
+ } catch (error) {
274
+ errors.push(error);
275
+ }
276
+ }
277
+ this.sessionDisposers.clear();
278
+ await this.flushQueue;
279
+ try {
280
+ await this.backend.forceFlush();
281
+ } catch (error) {
282
+ errors.push(error);
283
+ }
284
+ try {
285
+ await this.backend.shutdown();
286
+ } catch (error) {
287
+ errors.push(error);
288
+ } finally {
289
+ this.state = "closed";
290
+ }
291
+ if (errors.length === 1) throw errors[0];
292
+ if (errors.length > 1) throw new AggregateError(errors, "Langfuse runtime shutdown failed.");
293
+ }
294
+ };
295
+ function getLangfuseRuntimeInternal(runtime) {
296
+ const internal = runtime[RUNTIME_INTERNAL_KEY];
297
+ if (!internal) throw new Error("Langfuse runtime was not created by @narumitw/pi-langfuse.");
298
+ return internal;
299
+ }
300
+ function createLangfuseRuntimeFromBackend(backend) {
301
+ return registerLangfuseRuntime(new ManagedLangfuseRuntime(backend));
302
+ }
303
+ function registerLangfuseRuntime(runtime) {
304
+ Object.defineProperty(runtime, RUNTIME_INTERNAL_KEY, { value: runtime });
305
+ return runtime;
306
+ }
307
+
308
+ export {
309
+ DEFAULT_BASE_URL,
310
+ writeLangfuseConfig,
311
+ loadLangfuseConfig,
312
+ normalizeLangfuseConfig,
313
+ getLangfuseRuntimeInternal,
314
+ createLangfuseRuntimeFromBackend
315
+ };
316
+ //# sourceMappingURL=chunk-DBQAWNKG.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/config.ts", "../../src/runtime-core.ts"],
4
+ "sourcesContent": ["import { randomUUID } from \"node:crypto\";\nimport { chmod, mkdir, readFile, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"@earendil-works/pi-coding-agent\";\n\nconst CONFIG_FILE_NAME = \"pi-langfuse.json\";\nexport const DEFAULT_BASE_URL = \"https://us.cloud.langfuse.com\";\n\nexport interface LangfuseConfig {\n publicKey: string;\n secretKey: string;\n baseUrl: string;\n environment?: string;\n release?: string;\n userId?: string;\n captureContent: boolean;\n}\n\nexport type LangfuseConfigPatch = Partial<LangfuseConfig>;\n\nexport type LangfuseConfigResult =\n | { ok: true; config: LangfuseConfig; path: string; warnings: string[] }\n | { ok: false; path: string; warnings: string[]; reason: string };\n\nexport function langfuseConfigPath(): string {\n return join(getAgentDir(), CONFIG_FILE_NAME);\n}\n\nconst configSaveQueues = new Map<string, Promise<void>>();\n\nexport async function waitForLangfuseConfigWrites(path = langfuseConfigPath()): Promise<void> {\n await configSaveQueues.get(path);\n}\n\nexport function writeLangfuseConfig(patch: LangfuseConfigPatch, path = langfuseConfigPath()): Promise<LangfuseConfig> {\n const previous = configSaveQueues.get(path) ?? Promise.resolve();\n const operation = previous.then(() => writeLangfuseConfigNow(patch, path));\n const tail = operation.then(\n () => undefined,\n () => undefined,\n );\n configSaveQueues.set(path, tail);\n void tail.then(() => {\n if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);\n });\n return operation;\n}\n\nasync function writeLangfuseConfigNow(patch: LangfuseConfigPatch, path: string): Promise<LangfuseConfig> {\n const currentDocument = await readLangfuseDocumentForUpdate(path);\n const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });\n if (!normalized.ok) throw new Error(normalized.reason);\n const {\n publicKey: _publicKey,\n secretKey: _secretKey,\n baseUrl: _baseUrl,\n environment: _environment,\n release: _release,\n userId: _userId,\n captureContent: _captureContent,\n ...unknownFields\n } = currentDocument;\n const nextDocument = { ...unknownFields, ...normalized.config };\n\n await mkdir(dirname(path), { recursive: true, mode: 0o700 });\n const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}\\n`, {\n encoding: \"utf8\",\n mode: 0o600,\n });\n await chmod(tempPath, 0o600);\n await rename(tempPath, path);\n return normalized.config;\n } catch (error) {\n await rm(tempPath, { force: true }).catch(() => undefined);\n throw error;\n }\n}\n\nasync function readLangfuseDocumentForUpdate(path: string): Promise<Record<string, unknown>> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, \"utf8\")) as unknown;\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") return {};\n throw new Error(\n `Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`,\n );\n }\n const normalized = normalizeLangfuseConfig(parsed);\n if (!normalized.ok) {\n throw new Error(\n `Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`,\n );\n }\n return { ...(parsed as Record<string, unknown>) };\n}\n\nexport async function loadLangfuseConfig(path = langfuseConfigPath()): Promise<LangfuseConfigResult> {\n await configSaveQueues.get(path);\n const warnings: string[] = [];\n const permissionFailure = await ensurePrivatePermissions(path, warnings);\n if (permissionFailure) {\n return {\n ok: false,\n path,\n warnings,\n reason: `Refusing to load credentials: ${permissionFailure}`,\n };\n }\n\n let text: string;\n try {\n text = await readFile(path, \"utf8\");\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") {\n return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };\n }\n return {\n ok: false,\n path,\n warnings,\n reason: `Failed to read ${path}: ${formatError(error)}`,\n };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(text) as unknown;\n } catch {\n return {\n ok: false,\n path,\n warnings,\n reason: `Failed to parse ${path} as JSON; repair the file before retrying.`,\n };\n }\n\n const normalized = normalizeLangfuseConfig(parsed);\n if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };\n return { ok: true, config: normalized.config, path, warnings };\n}\n\nexport function normalizeLangfuseConfig(\n value: unknown,\n): { ok: true; config: LangfuseConfig } | { ok: false; reason: string } {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return { ok: false, reason: \"pi-langfuse.json must contain a JSON object.\" };\n }\n const input = value as Record<string, unknown>;\n const publicKey = normalizeString(input.publicKey);\n const secretKey = normalizeString(input.secretKey);\n if (!publicKey) {\n return { ok: false, reason: \"pi-langfuse.json requires a literal publicKey string.\" };\n }\n if (!secretKey) {\n return { ok: false, reason: \"pi-langfuse.json requires a literal secretKey string.\" };\n }\n if (isInterpolation(publicKey)) {\n return {\n ok: false,\n reason: \"pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported.\",\n };\n }\n if (isInterpolation(secretKey)) {\n return {\n ok: false,\n reason: \"pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported.\",\n };\n }\n\n const rawBaseUrl = input.baseUrl === undefined ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);\n if (!rawBaseUrl) {\n return { ok: false, reason: \"pi-langfuse.json baseUrl must be a non-empty string.\" };\n }\n const baseUrl = normalizeBaseUrl(rawBaseUrl);\n if (!baseUrl) {\n return {\n ok: false,\n reason: \"pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment.\",\n };\n }\n\n if (input.captureContent !== undefined && typeof input.captureContent !== \"boolean\") {\n return { ok: false, reason: \"pi-langfuse.json captureContent must be a boolean.\" };\n }\n const environment = optionalString(input.environment, \"environment\");\n if (!environment.ok) return environment;\n if (environment.value && (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))) {\n return {\n ok: false,\n reason:\n \"pi-langfuse.json environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse.\",\n };\n }\n const release = optionalString(input.release, \"release\");\n if (!release.ok) return release;\n const userId = optionalString(input.userId, \"userId\");\n if (!userId.ok) return userId;\n if (userId.value && userId.value.length > 200) {\n return { ok: false, reason: \"pi-langfuse.json userId must be at most 200 characters.\" };\n }\n\n return {\n ok: true,\n config: {\n publicKey,\n secretKey,\n baseUrl,\n ...(environment.value ? { environment: environment.value } : {}),\n ...(release.value ? { release: release.value } : {}),\n ...(userId.value ? { userId: userId.value } : {}),\n captureContent: input.captureContent ?? true,\n },\n };\n}\n\nasync function ensurePrivatePermissions(path: string, warnings: string[]): Promise<string | undefined> {\n try {\n const file = await stat(path);\n if ((file.mode & 0o777) !== 0o600) {\n await chmod(path, 0o600);\n const repaired = await stat(path);\n if ((repaired.mode & 0o777) !== 0o600) {\n throw new Error(`permissions remained ${(repaired.mode & 0o777).toString(8)}`);\n }\n warnings.push(`Restricted ${path} permissions to 0600.`);\n }\n } catch (error) {\n if (isNodeError(error) && error.code === \"ENOENT\") return undefined;\n const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;\n warnings.push(failure);\n return failure;\n }\n return undefined;\n}\n\nfunction normalizeBaseUrl(value: string): string | undefined {\n try {\n const url = new URL(value);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return undefined;\n if (url.username || url.password || url.search || url.hash) return undefined;\n return url.toString().replace(/\\/+$/, \"\");\n } catch {\n return undefined;\n }\n}\n\nfunction optionalString(value: unknown, name: string): { ok: true; value?: string } | { ok: false; reason: string } {\n if (value === undefined) return { ok: true };\n const normalized = normalizeString(value);\n return normalized\n ? { ok: true, value: normalized }\n : { ok: false, reason: `pi-langfuse.json ${name} must be a non-empty string.` };\n}\n\nfunction normalizeString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction isInterpolation(value: string): boolean {\n return value.startsWith(\"$\") || value.startsWith(\"!\");\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error;\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n", "import type { TraceBackend } from \"./tracing.js\";\n\nexport interface LangfuseRuntimeConfig {\n publicKey?: string;\n secretKey?: string;\n baseUrl?: string;\n environment?: string;\n release?: string;\n}\n\nexport interface CreateLangfuseRuntimeOptions {\n config?: LangfuseRuntimeConfig;\n env?: NodeJS.ProcessEnv | false;\n}\n\nexport interface LangfuseRuntime {\n readonly closed: boolean;\n flush(): Promise<void>;\n shutdown(): Promise<void>;\n}\n\ntype RuntimeSessionDisposer = (statusMessage: string) => void | Promise<void>;\n\nexport interface LangfuseRuntimeInternal extends LangfuseRuntime {\n readonly backend: TraceBackend;\n registerSession(disposer: RuntimeSessionDisposer): () => void;\n}\n\nconst RUNTIME_INTERNAL_KEY = Symbol.for(\"@narumitw/pi-langfuse/runtime-internal/v2\");\n\nclass ManagedLangfuseRuntime implements LangfuseRuntimeInternal {\n private state: \"open\" | \"closing\" | \"closed\" = \"open\";\n private readonly sessionDisposers = new Set<RuntimeSessionDisposer>();\n private flushQueue = Promise.resolve();\n private shutdownPromise: Promise<void> | undefined;\n\n constructor(readonly backend: TraceBackend) {}\n\n get closed(): boolean {\n return this.state !== \"open\";\n }\n\n registerSession(disposer: RuntimeSessionDisposer): () => void {\n if (this.state !== \"open\") throw new Error(\"Langfuse runtime is shutting down or already closed.\");\n this.sessionDisposers.add(disposer);\n return () => this.sessionDisposers.delete(disposer);\n }\n\n flush(): Promise<void> {\n if (this.state !== \"open\") {\n return Promise.reject(new Error(\"Langfuse runtime is shutting down or already closed.\"));\n }\n const operation = this.flushQueue.then(() => this.backend.forceFlush());\n this.flushQueue = operation.then(\n () => undefined,\n () => undefined,\n );\n return operation;\n }\n\n shutdown(): Promise<void> {\n if (this.shutdownPromise) return this.shutdownPromise;\n this.state = \"closing\";\n this.shutdownPromise = this.shutdownNow();\n return this.shutdownPromise;\n }\n\n private async shutdownNow(): Promise<void> {\n const errors: unknown[] = [];\n for (const dispose of [...this.sessionDisposers]) {\n try {\n await dispose(\"Langfuse runtime shut down before the Pi session was disposed.\");\n } catch (error) {\n errors.push(error);\n }\n }\n this.sessionDisposers.clear();\n\n await this.flushQueue;\n try {\n await this.backend.forceFlush();\n } catch (error) {\n errors.push(error);\n }\n try {\n await this.backend.shutdown();\n } catch (error) {\n errors.push(error);\n } finally {\n this.state = \"closed\";\n }\n\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, \"Langfuse runtime shutdown failed.\");\n }\n}\n\nexport function getLangfuseRuntimeInternal(runtime: LangfuseRuntime): LangfuseRuntimeInternal {\n const internal = (runtime as LangfuseRuntime & { [key: symbol]: LangfuseRuntimeInternal | undefined })[\n RUNTIME_INTERNAL_KEY\n ];\n if (!internal) throw new Error(\"Langfuse runtime was not created by @narumitw/pi-langfuse.\");\n return internal;\n}\n\nexport function createLangfuseRuntimeFromBackend(backend: TraceBackend): LangfuseRuntime {\n return registerLangfuseRuntime(new ManagedLangfuseRuntime(backend));\n}\n\nexport function registerLangfuseRuntime(runtime: LangfuseRuntimeInternal): LangfuseRuntimeInternal {\n Object.defineProperty(runtime, RUNTIME_INTERNAL_KEY, { value: runtime });\n return runtime;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }