@narumitw/pi-langfuse 0.50.5 → 0.51.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 CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-langfuse)](https://www.npmjs.com/package/@narumitw/pi-langfuse) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- Trace Pi agent runs, model generations, retries, tools, compaction, usage, and timing in [Langfuse](https://langfuse.com/) through OpenTelemetry.
5
+ Export Pi agent runs, model generations, retries, tools, compaction, usage, and timing to [Langfuse](https://langfuse.com/) through OpenTelemetry.
6
6
 
7
7
  ## ✨ Features
8
8
 
9
9
  - Groups each settled agent run into one trace with indexed attempt spans for retries and queued continuations.
10
- - Records bounded provider requests, final assistant output, identity, TTFT, usage, known costs, and safe response diagnostics.
10
+ - Records bounded provider requests, final assistant output, model identity, time to first content, usage, known costs, and safe response diagnostics.
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
@@ -26,24 +27,96 @@ Try the published package without installing it:
26
27
  pi -e npm:@narumitw/pi-langfuse
27
28
  ```
28
29
 
29
- Try a local checkout:
30
+ Build and load a local checkout from the repository root:
30
31
 
31
32
  ```bash
32
33
  npm --workspace @narumitw/pi-langfuse run build
33
34
  pi -e ./packages/pi-langfuse
34
35
  ```
35
36
 
36
- An unbuilt local checkout must be built before loading the package directory.
37
- The Langfuse v4 SDK requires Node.js 20 or newer.
37
+ The package declares `dist/index.ts`, so Pi cannot load an unbuilt local checkout.
38
+ The installed Langfuse SDK dependencies require Node.js 20 or newer.
39
+ Pi extensions run with your user permissions.
40
+ Review this extension and its data-export behavior before installing it.
38
41
 
39
42
  ## 🚀 Quick start
40
43
 
41
- Run `/langfuse`, choose the setup action, enter the Langfuse credentials, and restart Pi.
42
- Subsequent agent runs are traced according to the saved content-capture and metadata settings.
44
+ Run `/langfuse`, choose **Set up Langfuse for this Pi agent directory**, enter your credentials, and restart Pi.
45
+ New agent runs are then traced with the saved content-capture and metadata settings.
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.
43
116
 
44
117
  ## ⚙️ Settings
45
118
 
46
- Run the interactive manager, then choose **Set up Langfuse for this Pi agent directory** or **Update Langfuse for this Pi agent directory**:
119
+ Run the interactive manager and choose **Set up Langfuse for this Pi agent directory** or **Update Langfuse for this Pi agent directory**:
47
120
 
48
121
  ```text
49
122
  /langfuse
@@ -54,25 +127,24 @@ Leave either key blank to preserve its existing value when updating a valid conf
54
127
  Leave the base URL blank to use `https://us.cloud.langfuse.com`.
55
128
  The first successful setup creates the file.
56
129
  Within one Pi process, updates run in invocation order, reread the latest valid private document, preserve unknown fields, and save atomically with mode `0600`.
57
- Malformed JSON or invalid recognized fields block setup and update writes until repaired; errors redact entered and stored credentials.
58
- A failed save leaves the current session and prior file unchanged.
130
+ Malformed JSON or invalid recognized fields block writes until you repair the file.
131
+ Errors redact entered and stored credentials.
132
+ A failed save leaves the current session and previous file unchanged.
59
133
 
60
- Configuration belongs to the displayed Pi agent directory, not just the current conversation.
61
- Restart each running Pi process after saving; the new connection applies to subsequent sessions in that process.
134
+ Configuration applies to the displayed Pi agent directory, not only the current conversation.
135
+ Restart every running Pi process after saving so later sessions use the new connection.
62
136
  `/reload` is not sufficient because the isolated Langfuse runtime is initialized once per process.
63
137
  In print or JSON mode, edit the file manually because the interactive manager is unavailable.
64
138
 
65
- You can also create the file manually:
139
+ You can also create `<getAgentDir()>/pi-langfuse.json` (normally `~/.pi/agent/pi-langfuse.json`) manually.
140
+ This minimal example keeps prompts, responses, and tool content local:
66
141
 
67
142
  ```json
68
143
  {
69
144
  "publicKey": "pk-lf-...",
70
145
  "secretKey": "sk-lf-...",
71
146
  "baseUrl": "https://us.cloud.langfuse.com",
72
- "environment": "development",
73
- "release": "local",
74
- "userId": "your-user-id",
75
- "captureContent": true
147
+ "captureContent": false
76
148
  }
77
149
  ```
78
150
 
@@ -84,7 +156,9 @@ Prefer HTTPS because HTTP sends Langfuse credentials and trace content without t
84
156
  `environment`, `release`, and `userId` are optional Langfuse trace attributes.
85
157
  An environment must match Langfuse's contract: at most 40 lowercase letters, numbers, hyphens, or underscores, and it cannot start with `langfuse`.
86
158
  `userId` populates the Langfuse user dimension, which is what the Sessions and Traces views group by; Langfuse accepts at most 200 characters, and leaving it unset reports no user.
87
- Set `captureContent` to `false` to trace timing, model, usage, cost, status, and bounded diagnostic metadata without sending prompts, provider-request snapshots, responses, or tool content.
159
+ `captureContent` defaults to `true` when omitted.
160
+ Set it to `false` to trace only timing, model, usage, cost, status, and bounded diagnostic metadata.
161
+ In that mode, pi-langfuse does not export prompts, provider-request snapshots, responses, or tool content.
88
162
 
89
163
  The extension automatically restricts an existing config file to mode `0600` and refuses to load credentials if that protection cannot be enforced.
90
164
  You can also set it explicitly:
@@ -93,9 +167,10 @@ You can also set it explicitly:
93
167
  chmod 600 ~/.pi/agent/pi-langfuse.json
94
168
  ```
95
169
 
96
- Restart Pi after changing credentials, endpoint, environment, release, or `captureContent`.
170
+ Changes to credentials, endpoint, environment, release, or `captureContent` require a Pi process restart.
97
171
  Changes to `userId` apply to new sessions without restarting Pi.
98
- The isolated OpenTelemetry tracer provider is initialized once per Pi process and selected only for Langfuse; it does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
172
+ The isolated OpenTelemetry tracer provider is initialized once per Pi process and used only for Langfuse.
173
+ It does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
99
174
 
100
175
  ## 🔭 What is traced
101
176
 
@@ -148,9 +223,13 @@ Each `pi.llm` generation records:
148
223
  - additive input, output, cache-read, cache-write, and total token usage plus known positive input, output, cache-read, cache-write, and total cost buckets;
149
224
  - non-additive `pi.usage.reasoning_tokens` and `pi.usage.cache_write_1h_tokens` in metadata so subsets are not double-counted.
150
225
 
151
- The request snapshot is the payload visible at this handler, not a guaranteed final wire payload: later extensions can still replace it.
226
+ The request snapshot is the payload visible to this handler, not a guaranteed final wire payload.
227
+ A later extension can still replace it.
152
228
  Final assistant content is reconciled from `turn_end` and `agent_end` after message transformation.
153
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.
154
233
 
155
234
  ### Tool and compaction fields
156
235
 
@@ -162,7 +241,8 @@ Existing `pi.tool.call_id` and `pi.tool.name` correlation fields remain.
162
241
  `tool_execution_update` partial-result bodies are never captured.
163
242
  Duplicate, parallel, failed, no-progress, and interrupted tools are closed independently.
164
243
 
165
- An active `pi.compaction` records `pi.compaction.reason`, `pi.compaction.will_retry`, `pi.compaction.from_extension`, `pi.compaction.tokens_before`, `pi.compaction.messages_to_summarize`, `pi.compaction.turn_prefix_messages`, `pi.compaction.branch_entries`, and `pi.compaction.is_split_turn`.
244
+ An active `pi.compaction` records its reason, retry state, source, token count, message counts, branch-entry count, and split-turn state.
245
+ The exported field names are `pi.compaction.reason`, `pi.compaction.will_retry`, `pi.compaction.from_extension`, `pi.compaction.tokens_before`, `pi.compaction.messages_to_summarize`, `pi.compaction.turn_prefix_messages`, `pi.compaction.branch_entries`, and `pi.compaction.is_split_turn`.
166
246
  It adds `pi.compaction.read_file_count`, `pi.compaction.modified_file_count`, and `pi.compaction.usage.*` / `pi.compaction.cost.*` when Pi reports them.
167
247
  It never records the summary, custom instructions, or message bodies.
168
248
  Manual compaction outside an active agent trace is ignored; incomplete compaction closes as a warning at settlement or shutdown.
@@ -190,35 +270,23 @@ To wait for completed exports, run `/langfuse` and choose **Flush completed trac
190
270
 
191
271
  ## 💬 Commands
192
272
 
193
- ```text
194
- /langfuse
195
- ```
196
-
197
- The command opens one context-aware standard menu in TUI or RPC mode.
198
- Its state lines show the current session's tracing state, endpoint, content-capture mode, initialization failure when applicable, and private configuration path.
199
- It never displays credentials.
200
- Escape closes the menu.
201
- Credential and endpoint text inputs remain extension-owned because they enforce secret-preserving setup/update semantics rather than standard navigation.
273
+ Run `/langfuse` to configure tracing, inspect its state without exposing credentials, or flush completed traces for the current session in TUI or RPC mode.
274
+ Arguments are ignored for compatibility and cannot bypass the menu.
275
+ Print and JSON modes reject the command with an error containing tracing state and the manual configuration path.
202
276
 
203
- Available actions depend on that state:
204
-
205
- - **Flush completed traces for this session** appears first when tracing is active and waits for completed observations to export.
206
- - **Set up Langfuse for this Pi agent directory** appears when no valid config was loaded.
207
- - **Update Langfuse for this Pi agent directory** appears when a valid config exists.
208
- - **Show setup and privacy help** explains the agent-directory scope, manual configuration path, and content-capture risk.
209
-
210
- Connection actions state their agent-directory scope and per-process restart requirement before selection.
211
- Command arguments are intentionally ignored so remembered subcommands cannot silently bypass the menu.
212
- Print and JSON modes reject the interactive command with an observable error that includes current tracing state and the manual configuration path.
277
+ Connection changes apply to the current Pi agent directory and require each process to restart; see [Settings](#-settings).
278
+ Flushing exports completed observations to the configured endpoint; review [Security and privacy](#-security-and-privacy) before enabling content capture.
213
279
 
214
280
  ## 🔒 Security and privacy
215
281
 
216
282
  With content capture enabled, traces can contain user prompts, model responses, tool arguments, and tool results.
217
283
  These may include source code, file contents, shell output, or other sensitive project data.
218
- Review your Langfuse retention and access controls before enabling this extension.
284
+ Review Langfuse retention and access controls before enabling the extension.
219
285
 
220
- Git branch names, commit ids, working directory, session/leaf ids, the configured `userId`, model identity, usage/cost, aggregate counts, and allowlisted response-header values are metadata.
221
- They remain exported when `captureContent` is `false`; branch names and diagnostic header values can themselves contain operational details, and a `userId` may itself be personally identifying if you set it to an email address or a real name.
286
+ Metadata includes Git branch names, commit ids, working directory, session and leaf ids, the configured `userId`, model identity, usage, cost, aggregate counts, and allowlisted response-header values.
287
+ This metadata remains exported when `captureContent` is `false`.
288
+ Branch names and diagnostic header values can contain operational details.
289
+ A `userId` can identify you if you set it to an email address or real name.
222
290
  Choose a pseudonymous value when that matters, or leave `userId` unset to export no user at all.
223
291
 
224
292
  The built-in mask specifically protects Langfuse credentials; it is not a general secret scanner.
@@ -227,26 +295,21 @@ Compaction summaries, tool partial results, opaque continuation signatures, auth
227
295
 
228
296
  ## 🗂️ Package layout
229
297
 
230
- ```txt
298
+ ```text
231
299
  packages/pi-langfuse/
232
- ├── src/
233
- │ ├── index.ts # Thin repository entrypoint
234
- │ ├── langfuse.ts # Pi lifecycle integration and slash command
235
- │ ├── tracing.ts # Observation lifecycle, outcomes, and bounded metadata
236
- │ ├── sanitizer.ts # Content bounding and opaque-signature removal
237
- │ ├── runtime.ts # Lazy Langfuse/OpenTelemetry runtime
238
- │ └── config.ts # Private pi-langfuse.json loading and validation
239
- ├── dist/ # Generated Jiti runtime and lazy chunks
240
- ├── scripts/build-runtime.mjs
241
- ├── test/
242
- ├── README.md
243
- ├── LICENSE
244
- ├── tsconfig.json
245
- └── package.json
300
+ ├── src/ # Authoritative implementation and helpers
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
308
+ ├── scripts/build-runtime.mjs # Runtime builder
309
+ └── test/ # Behavior and lifecycle coverage
246
310
  ```
247
311
 
248
- The package build generates the published Pi entrypoint at `dist/index.ts` and preserves `runtime.ts` as a first-use chunk.
249
- The source modules remain authoritative.
312
+ The generated runtime is built from `src/index.ts` and does not import back into `src`.
250
313
 
251
314
  ## 🔎 Keywords
252
315
 
@@ -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-IASLJKKB.js.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
+ }