@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 +125 -62
- package/dist/chunks/chunk-IASLJKKB.js +316 -0
- package/dist/chunks/chunk-IASLJKKB.js.map +7 -0
- package/dist/chunks/runtime-6MIKLWNJ.js +352 -0
- package/dist/chunks/runtime-6MIKLWNJ.js.map +7 -0
- package/dist/config.d.ts +33 -0
- package/dist/git.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1551 -0
- package/dist/index.js.map +7 -0
- package/dist/index.ts +594 -521
- package/dist/index.ts.map +4 -4
- package/dist/langfuse.d.ts +13 -0
- package/dist/pi-session.d.ts +43 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/runtime-core.d.ts +26 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/sanitizer.d.ts +2 -0
- package/dist/tracing.d.ts +202 -0
- package/package.json +63 -59
- package/src/config.ts +211 -232
- package/src/git.ts +60 -0
- package/src/index.ts +9 -0
- package/src/langfuse.ts +307 -563
- package/src/pi-session.ts +570 -0
- package/src/public-api.ts +17 -0
- package/src/runtime-core.ts +113 -0
- package/src/runtime.ts +393 -227
- package/src/sanitizer.ts +115 -117
- package/src/tracing.ts +828 -843
- package/dist/chunks/runtime-247EMSG3.js +0 -218
- package/dist/chunks/runtime-247EMSG3.js.map +0 -7
package/README.md
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@narumitw/pi-langfuse) [](https://pi.dev) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
|
|
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,
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
The Langfuse
|
|
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
|
|
42
|
-
|
|
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
|
|
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
|
|
58
|
-
|
|
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
|
|
61
|
-
Restart
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
284
|
+
Review Langfuse retention and access controls before enabling the extension.
|
|
219
285
|
|
|
220
|
-
Git branch names, commit ids, working directory, session
|
|
221
|
-
|
|
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
|
-
```
|
|
298
|
+
```text
|
|
231
299
|
packages/pi-langfuse/
|
|
232
|
-
├── src/
|
|
233
|
-
│ ├── index.ts
|
|
234
|
-
│ ├── langfuse.ts
|
|
235
|
-
│ ├──
|
|
236
|
-
│ ├──
|
|
237
|
-
│ ├── runtime.ts
|
|
238
|
-
│ └──
|
|
239
|
-
├── dist/
|
|
240
|
-
├── scripts/build-runtime.mjs
|
|
241
|
-
|
|
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
|
|
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
|
+
}
|