@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/dist/index.ts CHANGED
@@ -1,232 +1,65 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
3
+ import {
4
+ DEFAULT_BASE_URL,
5
+ createLangfuseRuntimeFromBackend,
6
+ getLangfuseRuntimeInternal,
7
+ loadLangfuseConfig,
8
+ normalizeLangfuseConfig,
9
+ writeLangfuseConfig
10
+ } from "./chunks/chunk-DBQAWNKG.ts";
3
11
 
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))) {
12
+ // src/git.ts
13
+ var GIT_LOOKUP_TIMEOUT_MS = 1e3;
14
+ var MAX_GIT_BRANCH_LENGTH = 256;
15
+ async function resolveGitMetadata(exec, cwd) {
16
+ const [branchResult, commit] = await Promise.all([resolveGitBranch(exec, cwd), resolveGitCommit(exec, cwd)]);
17
+ if (!branchResult.resolved) return void 0;
18
+ if (branchResult.branch) {
160
19
  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."
20
+ branch: branchResult.branch,
21
+ ...commit ? { commit } : {},
22
+ detached: false
163
23
  };
164
24
  }
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
- };
25
+ return commit ? { commit, detached: true } : void 0;
184
26
  }
185
- async function ensurePrivatePermissions(path, warnings) {
27
+ async function resolveGitBranch(exec, cwd) {
186
28
  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;
29
+ const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
30
+ cwd,
31
+ timeout: GIT_LOOKUP_TIMEOUT_MS
32
+ });
33
+ if (result.killed) return { resolved: false };
34
+ if (result.code === 1) return { resolved: true };
35
+ if (result.code !== 0) return { resolved: false };
36
+ const branch = normalizeGitBranch(result.stdout);
37
+ return branch ? { resolved: true, branch } : { resolved: false };
38
+ } catch {
39
+ return { resolved: false };
201
40
  }
202
- return void 0;
203
41
  }
204
- function normalizeBaseUrl(value) {
42
+ async function resolveGitCommit(exec, cwd) {
205
43
  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(/\/+$/, "");
44
+ const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
45
+ cwd,
46
+ timeout: GIT_LOOKUP_TIMEOUT_MS
47
+ });
48
+ if (result.code !== 0 || result.killed) return void 0;
49
+ const commit = result.stdout.trim();
50
+ return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : void 0;
210
51
  } catch {
211
52
  return void 0;
212
53
  }
213
54
  }
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);
55
+ function normalizeGitBranch(value) {
56
+ const branch = value.trim();
57
+ if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return void 0;
58
+ for (const character of branch) {
59
+ const code = character.codePointAt(0) ?? 0;
60
+ if (code <= 31 || code === 127) return void 0;
61
+ }
62
+ return branch;
230
63
  }
231
64
 
232
65
  // src/sanitizer.ts
@@ -249,9 +82,7 @@ function sanitize(value, active, depth, budget) {
249
82
  return consume(value, budget);
250
83
  }
251
84
  if (typeof value === "string") {
252
- const bounded = truncateString(value, Math.min(MAX_STRING_LENGTH, budget.remaining));
253
- const redacted = bounded.replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
254
- return consume(redacted, budget);
85
+ return consume(sanitizeString(value, Math.min(MAX_STRING_LENGTH, budget.remaining)), budget);
255
86
  }
256
87
  if (typeof value === "bigint") return consume(value.toString(), budget);
257
88
  if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
@@ -301,12 +132,13 @@ function sanitize(value, active, depth, budget) {
301
132
  omitted = true;
302
133
  break;
303
134
  }
304
- budget.remaining -= keyBytes + 4;
135
+ const sanitizedKey = sanitizeString(key, keyBudget);
136
+ budget.remaining -= byteLength(sanitizedKey) + 4;
305
137
  const property = readProperty(record, key);
306
138
  if (redactData && key === "data") {
307
- output[key] = consume("[base64 omitted]", budget);
308
- } else if (!property.ok) output[key] = consume("[unreadable property]", budget);
309
- else output[key] = sanitize(property.value, active, depth + 1, budget);
139
+ output[sanitizedKey] = consume("[base64 omitted]", budget);
140
+ } else if (!property.ok) output[sanitizedKey] = consume("[unreadable property]", budget);
141
+ else output[sanitizedKey] = sanitize(property.value, active, depth + 1, budget);
310
142
  processed += 1;
311
143
  }
312
144
  } catch {
@@ -338,6 +170,9 @@ function consume(value, budget) {
338
170
  budget.remaining -= size;
339
171
  return value;
340
172
  }
173
+ function sanitizeString(value, maxBytes) {
174
+ return truncateString(value, maxBytes).replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
175
+ }
341
176
  function truncateString(value, maxBytes) {
342
177
  const boundedPrefix = value.slice(0, maxBytes);
343
178
  if (boundedPrefix.length === value.length && byteLength(boundedPrefix) <= maxBytes) {
@@ -432,6 +267,7 @@ var TraceRecorder = class {
432
267
  ...hasItems(input.images) ? { images: input.images } : {}
433
268
  });
434
269
  const metadata = {
270
+ ...customTraceMetadata(this.context.metadata),
435
271
  "pi.cwd": this.context.cwd,
436
272
  ...input.git?.branch ? { "pi.git.branch": input.git.branch } : {},
437
273
  ...input.git?.commit ? { "pi.git.commit": input.git.commit } : {},
@@ -439,6 +275,7 @@ var TraceRecorder = class {
439
275
  "pi.mode": this.context.mode,
440
276
  ...input.model?.id ? { "pi.model": input.model.id } : {},
441
277
  ...input.model?.provider ? { "pi.provider": input.model.provider } : {},
278
+ ...input.requestId ? { "pi.request.id": input.requestId } : {},
442
279
  "pi.session.id": this.context.sessionId,
443
280
  "pi.trace.schema_version": Number(TRACE_SCHEMA_VERSION),
444
281
  ...snapshotMetadata("start", input.snapshot)
@@ -447,14 +284,21 @@ var TraceRecorder = class {
447
284
  const gitTag = input.git?.branch ? `branch:${input.git.branch}` : input.git?.detached ? "git:detached" : void 0;
448
285
  this.root = this.startObservation("pi.agent", attributes, { asType: "agent" });
449
286
  this.root.updateTrace?.({
450
- name: "pi.trace",
287
+ name: normalizeTraceName(this.context.traceName),
451
288
  sessionId: this.context.sessionId,
452
289
  ...this.context.userId ? { userId: this.context.userId } : {},
453
290
  version: TRACE_SCHEMA_VERSION,
454
291
  input: traceInput,
455
292
  metadata,
456
- tags: ["pi", ...gitTag ? [gitTag] : []]
293
+ tags: traceTags(this.context.tags, gitTag)
457
294
  });
295
+ const traceId = this.root.traceId;
296
+ if (traceId && this.context.onTraceId) {
297
+ try {
298
+ void Promise.resolve(this.context.onTraceId(traceId)).catch(() => void 0);
299
+ } catch {
300
+ }
301
+ }
458
302
  }
459
303
  beginAttempt(input = {}) {
460
304
  if (!this.root) return;
@@ -544,7 +388,11 @@ var TraceRecorder = class {
544
388
  ...Object.keys(requestMetadata).length > 0 ? { metadata: requestMetadata } : {},
545
389
  version: TRACE_SCHEMA_VERSION
546
390
  },
547
- { asType: "generation", parent: this.turn ?? this.attempt ?? this.root }
391
+ {
392
+ asType: "generation",
393
+ parent: this.turn ?? this.attempt ?? this.root,
394
+ ...input.startedAt === void 0 ? {} : { startTime: new Date(input.startedAt) }
395
+ }
548
396
  );
549
397
  this.generation = {
550
398
  observation,
@@ -746,12 +594,8 @@ var TraceRecorder = class {
746
594
  interrupt(statusMessage, snapshot) {
747
595
  this.closeActiveTrace(statusMessage, snapshot, "interrupted");
748
596
  }
749
- async flush() {
750
- await this.backend.forceFlush();
751
- }
752
- async shutdown(snapshot) {
753
- this.closeActiveTrace("Pi shut down before the active trace settled.", snapshot, "interrupted");
754
- await this.backend.shutdown();
597
+ dispose(statusMessage = "Pi session ended before the active trace settled.", snapshot) {
598
+ this.closeActiveTrace(statusMessage, snapshot, "interrupted");
755
599
  }
756
600
  closeActiveTrace(statusMessage, snapshot, forcedOutcome) {
757
601
  this.closeTurn(statusMessage ?? "Pi turn ended when the agent run settled.");
@@ -874,6 +718,24 @@ var TraceRecorder = class {
874
718
  return this.context.captureContent ? sanitizeTraceValue(value) : CONTENT_DISABLED;
875
719
  }
876
720
  };
721
+ function normalizeTraceName(value) {
722
+ return typeof value === "string" && value.trim() ? value.trim() : "pi.trace";
723
+ }
724
+ function traceTags(customTags, gitTag) {
725
+ const tags = ["pi", ...gitTag ? [gitTag] : [], ...customTags ?? []];
726
+ return [...new Set(tags.map(sanitizeTraceTag).filter((tag) => tag !== void 0))];
727
+ }
728
+ function sanitizeTraceTag(value) {
729
+ if (typeof value !== "string" || !value.trim()) return void 0;
730
+ const sanitized = sanitizeTraceValue(value.trim());
731
+ return typeof sanitized === "string" && sanitized.trim() ? sanitized.trim() : void 0;
732
+ }
733
+ function customTraceMetadata(metadata) {
734
+ if (!metadata) return {};
735
+ const sanitized = sanitizeTraceValue(metadata);
736
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) return {};
737
+ return Object.fromEntries(Object.entries(sanitized).filter(([key]) => !key.startsWith("pi.")));
738
+ }
877
739
  function generationHttpMetadata(state) {
878
740
  const finalStatus = state.statuses.at(-1);
879
741
  return {
@@ -986,227 +848,258 @@ function emptyCounters() {
986
848
  };
987
849
  }
988
850
 
989
- // src/langfuse.ts
990
- var FLUSH_ACTION = "Flush completed traces for this session";
991
- var SET_UP_ACTION = "Set up Langfuse for this Pi agent directory (restart required)";
992
- var UPDATE_ACTION = "Update Langfuse for this Pi agent directory (restart required)";
993
- var HELP_ACTION = "Show setup and privacy help";
994
- function createLangfuseExtension(dependencies = {}) {
995
- const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
996
- const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
997
- const createBackend = dependencies.createBackend ?? (async (config) => {
998
- const { createProductionBackend } = await import("./chunks/runtime-X75FRK2E.js");
999
- return createProductionBackend(config);
851
+ // src/pi-session.ts
852
+ function createPiLangfuseSession(runtime, options = {}) {
853
+ const sessionOptions = snapshotSessionOptions(options);
854
+ return createPiLangfuseSessionController({
855
+ resolveSession: async () => ({ runtime, options: sessionOptions })
1000
856
  });
1001
- return function langfuse(pi) {
1002
- const resolveGit = dependencies.resolveGitMetadata ?? ((cwd) => resolveGitMetadata((command, args, options) => pi.exec(command, args, options), cwd));
1003
- let recorder;
1004
- let activeConfig;
1005
- let configPath;
1006
- let initializationError;
1007
- let hasStoredConfig = false;
1008
- let configurationNotice;
1009
- let sessionGeneration = 0;
1010
- let menuController = new AbortController();
1011
- let configWriteQueue = Promise.resolve();
1012
- let nextAttemptReason;
1013
- async function showLangfuseMenu(ctx) {
1014
- if (!ctx.hasUI) {
1015
- ctx.ui.notify(formatNonInteractiveStatus(activeConfig, configPath, initializationError), "warning");
1016
- return;
857
+ }
858
+ function createPiLangfuseSessionController(options) {
859
+ let binding;
860
+ let runtimeForShutdown;
861
+ let ownerRegistration;
862
+ let pendingInitialization;
863
+ const outstandingInitializations = /* @__PURE__ */ new Set();
864
+ let disposePromise;
865
+ let disposed = false;
866
+ let sessionGeneration = 0;
867
+ let requestId;
868
+ let requestIdRevision = 0;
869
+ let nextAttemptReason;
870
+ let lastSnapshot;
871
+ let pendingProviderGeneration;
872
+ const controller = {
873
+ extension(pi) {
874
+ if (disposed) throw new Error("A disposed Pi Langfuse session controller cannot be bound.");
875
+ registerHooks(pi);
876
+ },
877
+ setRequestId(value) {
878
+ requestId = normalizeOptionalString(value);
879
+ requestIdRevision += 1;
880
+ },
881
+ dispose() {
882
+ if (disposePromise) return disposePromise;
883
+ disposed = true;
884
+ const initializations = invalidateAllInitializations("disposed");
885
+ ownerRegistration = void 0;
886
+ sessionGeneration += 1;
887
+ closeBinding(binding, "Pi Langfuse session controller was disposed.", lastSnapshot);
888
+ runtimeForShutdown = void 0;
889
+ disposePromise = joinInitializations(initializations);
890
+ return disposePromise;
891
+ },
892
+ get active() {
893
+ return binding !== void 0;
894
+ },
895
+ get runtime() {
896
+ return binding?.runtime;
897
+ },
898
+ async flush() {
899
+ const runtime = binding?.runtime;
900
+ if (!runtime) throw new Error("Langfuse tracing is not enabled for this session.");
901
+ await runtime.flush();
902
+ }
903
+ };
904
+ function closeBinding(target, statusMessage, snapshot) {
905
+ if (!target) return;
906
+ const wasCurrent = binding === target;
907
+ if (wasCurrent) {
908
+ binding = void 0;
909
+ pendingProviderGeneration = void 0;
910
+ }
911
+ target.releaseRuntime();
912
+ target.recorder.dispose(statusMessage, snapshot);
913
+ if (wasCurrent) nextAttemptReason = void 0;
914
+ }
915
+ function invalidatePendingInitialization(reason) {
916
+ if (!pendingInitialization) return;
917
+ pendingInitialization.current = false;
918
+ pendingInitialization.reason = reason;
919
+ pendingInitialization = void 0;
920
+ }
921
+ function invalidateAllInitializations(reason) {
922
+ const initializations = [...outstandingInitializations];
923
+ for (const initialization of initializations) {
924
+ initialization.current = false;
925
+ initialization.reason = reason;
926
+ }
927
+ pendingInitialization = void 0;
928
+ return initializations;
929
+ }
930
+ function createPendingInitialization() {
931
+ let complete;
932
+ const completion = new Promise((resolve) => {
933
+ complete = resolve;
934
+ });
935
+ const initialization = { current: true, completion, complete };
936
+ outstandingInitializations.add(initialization);
937
+ return initialization;
938
+ }
939
+ async function joinInitializations(initializations) {
940
+ const results = await Promise.all(initializations.map(({ completion }) => completion));
941
+ const errors = results.flatMap((result) => result.ok ? [] : [result.error]);
942
+ if (errors.length === 1) throw errors[0];
943
+ if (errors.length > 1) throw new AggregateError(errors, "Pi Langfuse session initialization cleanup failed.");
944
+ }
945
+ async function initializeSession(registration, initialization, ctx) {
946
+ const generation = ++sessionGeneration;
947
+ closeBinding(binding, "Pi session was replaced before shutdown completed.", lastSnapshot);
948
+ lastSnapshot = contextSnapshot(ctx);
949
+ nextAttemptReason = void 0;
950
+ options.onSessionStart?.();
951
+ const isCurrent = () => !disposed && initialization.current && ownerRegistration === registration && generation === sessionGeneration;
952
+ let resolved;
953
+ try {
954
+ resolved = await options.resolveSession(ctx, isCurrent);
955
+ } catch (error) {
956
+ if (isCurrent()) options.onInitializationError?.(error, ctx);
957
+ return;
958
+ }
959
+ if (!isCurrent()) {
960
+ const reason = initialization.reason ?? "replaced";
961
+ if (resolved && options.shutdownRuntimeOnQuit && reason !== "quit" && reason !== "disposed" && (!runtimeForShutdown || runtimeForShutdown.closed)) {
962
+ runtimeForShutdown = resolved.runtime;
1017
963
  }
1018
- const menuGeneration = sessionGeneration;
1019
- const menuSignal = menuController.signal;
1020
- const isCurrent = () => menuGeneration === sessionGeneration && !menuSignal.aborted;
1021
- const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
1022
- if (!isCurrent()) return;
1023
- const menu = defineMenu({
1024
- start: "main",
1025
- screens: {
1026
- main: () => ({
1027
- kind: "actions",
1028
- title: "Langfuse",
1029
- lines: formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice).split("\n").slice(1),
1030
- items: [
1031
- ...recorder ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" }] : [],
1032
- {
1033
- id: "configure",
1034
- label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
1035
- action: "configure"
1036
- },
1037
- { id: "help", label: HELP_ACTION, action: "help" }
1038
- ],
1039
- hint: "close"
1040
- })
1041
- },
1042
- actions: {
1043
- flush: async () => {
1044
- const menuRecorder = recorder;
1045
- const menuConfig = activeConfig;
1046
- if (!menuRecorder) {
1047
- ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
1048
- return { kind: "close" };
1049
- }
1050
- try {
1051
- await menuRecorder.flush();
1052
- if (menuGeneration !== sessionGeneration) return { kind: "close" };
1053
- ctx.ui.notify("Langfuse traces flushed for this session.", "info");
1054
- } catch (error) {
1055
- if (menuGeneration !== sessionGeneration) return { kind: "close" };
1056
- ctx.ui.notify(`Langfuse flush failed: ${formatError2(error, menuConfig)}`, "error");
1057
- }
1058
- return { kind: "close" };
1059
- },
1060
- configure: async () => {
1061
- const loaded = await loadConfig(configPath);
1062
- if (menuGeneration !== sessionGeneration) return { kind: "close" };
1063
- configPath = loaded.path;
1064
- const next = await promptForConfig(
1065
- ctx,
1066
- loaded.ok ? loaded.config : void 0,
1067
- () => menuGeneration === sessionGeneration
1068
- );
1069
- if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
1070
- try {
1071
- const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
1072
- configWriteQueue = write.then(
1073
- () => void 0,
1074
- () => void 0
1075
- );
1076
- await write;
1077
- if (menuGeneration !== sessionGeneration) return { kind: "close" };
1078
- hasStoredConfig = true;
1079
- configurationNotice = "Saved; restart each Pi process to use it in subsequent sessions.";
1080
- ctx.ui.notify(
1081
- `Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
1082
- "info"
1083
- );
1084
- } catch (error) {
1085
- if (menuGeneration !== sessionGeneration) return { kind: "close" };
1086
- ctx.ui.notify(`Failed to save Langfuse config: ${formatError2(error, next)}`, "error");
1087
- }
1088
- return { kind: "close" };
1089
- },
1090
- help: async () => {
1091
- ctx.ui.notify(formatHelp(configPath), "info");
1092
- return { kind: "close" };
1093
- }
1094
- }
1095
- });
1096
- await runMenu(ctx, menu, {
1097
- getState: () => void 0,
1098
- signal: menuSignal,
1099
- isCurrent
964
+ await resolved?.releaseIfStale?.(reason);
965
+ return;
966
+ }
967
+ if (!resolved) {
968
+ options.onSessionUnavailable?.();
969
+ return;
970
+ }
971
+ try {
972
+ const internal = getLangfuseRuntimeInternal(resolved.runtime);
973
+ const recorderOptions = createRecorderOptions(ctx, resolved.options);
974
+ const recorder = new TraceRecorder(internal.backend, recorderOptions);
975
+ const nextBinding = {};
976
+ const releaseRuntime = internal.registerSession((statusMessage) => {
977
+ closeBinding(nextBinding, statusMessage, lastSnapshot);
1100
978
  });
979
+ Object.assign(nextBinding, { registration, recorder, runtime: resolved.runtime, releaseRuntime });
980
+ if (!isCurrent()) {
981
+ releaseRuntime();
982
+ return;
983
+ }
984
+ binding = nextBinding;
985
+ if (options.shutdownRuntimeOnQuit) runtimeForShutdown = resolved.runtime;
986
+ options.onSessionReady?.(resolved);
987
+ } catch (error) {
988
+ if (isCurrent()) options.onInitializationError?.(error, ctx);
1101
989
  }
1102
- pi.registerCommand("langfuse", {
1103
- description: "Open interactive Langfuse tracing controls",
1104
- handler: async (_args, ctx) => showLangfuseMenu(ctx)
1105
- });
990
+ }
991
+ function registerHooks(pi) {
992
+ const registration = {};
993
+ const activeRecorder = () => activeRecorderFor(registration);
1106
994
  pi.on("session_start", async (_event, ctx) => {
1107
- const generation = ++sessionGeneration;
1108
- menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
1109
- menuController = new AbortController();
1110
- recorder = void 0;
1111
- activeConfig = void 0;
1112
- configPath = void 0;
1113
- initializationError = void 0;
1114
- hasStoredConfig = false;
1115
- configurationNotice = void 0;
1116
- nextAttemptReason = void 0;
1117
- await configWriteQueue;
1118
- if (generation !== sessionGeneration) return;
1119
- const result = await loadConfig();
1120
- if (generation !== sessionGeneration) return;
1121
- configPath = result.path;
1122
- for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
1123
- if (!result.ok) {
1124
- initializationError = formatConfigError(result);
1125
- ctx.ui.notify(initializationError, "warning");
995
+ if (disposed) return;
996
+ pendingProviderGeneration = void 0;
997
+ if (ownerRegistration && ownerRegistration !== registration) {
998
+ options.onInitializationError?.(
999
+ new Error("A Pi Langfuse session controller cannot manage multiple active Pi sessions."),
1000
+ ctx
1001
+ );
1126
1002
  return;
1127
1003
  }
1128
- hasStoredConfig = true;
1004
+ invalidatePendingInitialization("replaced");
1005
+ ownerRegistration = registration;
1006
+ const initialization = createPendingInitialization();
1007
+ pendingInitialization = initialization;
1008
+ let result = { ok: true };
1129
1009
  try {
1130
- const backend = await createBackend(result.config);
1131
- if (generation !== sessionGeneration) {
1132
- await backend.shutdown().catch(() => void 0);
1133
- return;
1134
- }
1135
- activeConfig = result.config;
1136
- recorder = new TraceRecorder(backend, {
1137
- sessionId: ctx.sessionManager.getSessionId(),
1138
- ...result.config.userId ? { userId: result.config.userId } : {},
1139
- cwd: ctx.cwd,
1140
- mode: ctx.mode,
1141
- captureContent: result.config.captureContent
1142
- });
1010
+ await initializeSession(registration, initialization, ctx);
1143
1011
  } catch (error) {
1144
- if (generation !== sessionGeneration) return;
1145
- initializationError = `Langfuse tracing could not start: ${formatError2(error, result.config)}`;
1146
- ctx.ui.notify(initializationError, "warning");
1012
+ result = { ok: false, error };
1013
+ throw error;
1014
+ } finally {
1015
+ initialization.complete(result);
1016
+ outstandingInitializations.delete(initialization);
1017
+ if (pendingInitialization === initialization) pendingInitialization = void 0;
1147
1018
  }
1148
1019
  });
1149
1020
  pi.on("before_agent_start", async (event, ctx) => {
1021
+ pendingProviderGeneration = void 0;
1022
+ const active = ownerRegistration === registration ? binding : void 0;
1023
+ if (!active || active.runtime.closed) return;
1150
1024
  nextAttemptReason = void 0;
1151
- const activeRecorder = recorder;
1152
- if (!activeRecorder) return;
1153
- const git = await resolveGit(ctx.cwd).catch(() => void 0);
1154
- if (recorder !== activeRecorder) return;
1155
- activeRecorder.beginAgent({
1156
- prompt: event.prompt,
1157
- images: event.images,
1158
- model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
1159
- git,
1160
- snapshot: contextSnapshot(ctx)
1025
+ const git = await (options.resolveGitMetadata ? options.resolveGitMetadata(pi, ctx.cwd) : resolveGitMetadata((command, args, execOptions) => pi.exec(command, args, execOptions), ctx.cwd)).catch(() => void 0);
1026
+ if (ownerRegistration !== registration || binding !== active || active.runtime.closed) return;
1027
+ lastSnapshot = contextSnapshot(ctx);
1028
+ startRootTrace((nextRequestId) => {
1029
+ active.recorder.beginAgent({
1030
+ prompt: event.prompt,
1031
+ images: event.images,
1032
+ model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
1033
+ git,
1034
+ snapshot: lastSnapshot,
1035
+ ...nextRequestId ? { requestId: nextRequestId } : {}
1036
+ });
1161
1037
  });
1162
1038
  });
1163
1039
  pi.on("agent_start", () => {
1040
+ const recorder = activeRecorder();
1164
1041
  if (!recorder) return;
1165
1042
  recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : void 0);
1166
1043
  nextAttemptReason = void 0;
1167
1044
  });
1168
1045
  pi.on("turn_start", (event, ctx) => {
1046
+ const recorder = activeRecorder();
1169
1047
  if (!recorder) return;
1170
- ensureActiveRun(recorder, ctx);
1048
+ lastSnapshot = contextSnapshot(ctx);
1049
+ ensureActiveRun(recorder, ctx, startRootTrace);
1171
1050
  recorder.beginTurn(event.turnIndex);
1172
1051
  });
1173
1052
  pi.on("before_provider_request", (event, ctx) => {
1174
- if (!recorder) return;
1175
- ensureActiveRun(recorder, ctx);
1176
- recorder.beginGeneration({
1053
+ if (!activeRecorder()) return;
1054
+ lastSnapshot = contextSnapshot(ctx);
1055
+ pendingProviderGeneration = {
1056
+ startedAt: Date.now(),
1177
1057
  payload: event.payload,
1178
- payloadStage: "before_provider_request",
1179
1058
  model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
1180
- thinkingLevel: pi.getThinkingLevel()
1181
- });
1059
+ thinkingLevel: pi.getThinkingLevel(),
1060
+ responses: []
1061
+ };
1182
1062
  });
1183
1063
  pi.on("after_provider_response", (event) => {
1184
- recorder?.recordProviderResponse(event.status, event.headers);
1064
+ pendingProviderGeneration?.responses.push({ status: event.status, headers: event.headers });
1065
+ });
1066
+ pi.on("message_start", (event, ctx) => {
1067
+ if (event.message.role === "assistant") claimPendingProviderGeneration(ctx, activeRecorder());
1185
1068
  });
1186
- pi.on("message_update", (event) => {
1187
- if (isRealOutputDelta(event.assistantMessageEvent)) recorder?.markGenerationFirstOutput();
1069
+ pi.on("message_update", (event, ctx) => {
1070
+ if (!isRealOutputDelta(event.assistantMessageEvent)) return;
1071
+ const recorder = activeRecorder();
1072
+ claimPendingProviderGeneration(ctx, recorder);
1073
+ recorder?.markGenerationFirstOutput();
1188
1074
  });
1189
- pi.on("message_end", (event) => {
1190
- if (event.message.role === "assistant") recorder?.markGenerationEnd();
1075
+ pi.on("message_end", (event, ctx) => {
1076
+ if (event.message.role !== "assistant") return;
1077
+ const recorder = activeRecorder();
1078
+ claimPendingProviderGeneration(ctx, recorder);
1079
+ recorder?.markGenerationEnd();
1191
1080
  });
1192
- pi.on("turn_end", (event) => {
1193
- if (event.message.role === "assistant") recorder?.finishAssistant(event.message);
1194
- recorder?.finishTurn(event.turnIndex, {
1081
+ pi.on("turn_end", (event, ctx) => {
1082
+ const recorder = activeRecorder();
1083
+ if (!recorder) return;
1084
+ if (event.message.role === "assistant") claimPendingProviderGeneration(ctx, recorder);
1085
+ lastSnapshot = contextSnapshot(ctx);
1086
+ if (event.message.role === "assistant") recorder.finishAssistant(event.message);
1087
+ recorder.finishTurn(event.turnIndex, {
1195
1088
  message: event.message,
1196
1089
  toolResultCount: event.toolResults.length
1197
1090
  });
1198
1091
  });
1199
1092
  pi.on("tool_execution_start", (event) => {
1200
- recorder?.beginTool(event.toolCallId, event.toolName, event.args);
1093
+ activeRecorder()?.beginTool(event.toolCallId, event.toolName, event.args);
1201
1094
  });
1202
1095
  pi.on("tool_execution_update", (event) => {
1203
- recorder?.recordToolProgress(event.toolCallId);
1096
+ activeRecorder()?.recordToolProgress(event.toolCallId);
1204
1097
  });
1205
1098
  pi.on("tool_result", (event) => {
1206
- recorder?.recordToolInput(event.toolCallId, event.input);
1099
+ activeRecorder()?.recordToolInput(event.toolCallId, event.input);
1207
1100
  });
1208
1101
  pi.on("tool_execution_end", (event) => {
1209
- recorder?.finishTool(event.toolCallId, {
1102
+ activeRecorder()?.finishTool(event.toolCallId, {
1210
1103
  content: event.result.content,
1211
1104
  details: event.result.details,
1212
1105
  isError: event.isError
@@ -1214,10 +1107,10 @@ function createLangfuseExtension(dependencies = {}) {
1214
1107
  });
1215
1108
  pi.on("agent_end", (event) => {
1216
1109
  const message = findLastAssistant(event.messages);
1217
- recorder?.finishAttempt(message);
1110
+ activeRecorder()?.finishAttempt(message);
1218
1111
  });
1219
1112
  pi.on("session_before_compact", (event) => {
1220
- recorder?.beginCompaction({
1113
+ activeRecorder()?.beginCompaction({
1221
1114
  reason: event.reason,
1222
1115
  willRetry: event.willRetry,
1223
1116
  tokensBefore: event.preparation.tokensBefore,
@@ -1228,6 +1121,7 @@ function createLangfuseExtension(dependencies = {}) {
1228
1121
  });
1229
1122
  });
1230
1123
  pi.on("session_compact", (event) => {
1124
+ const recorder = activeRecorder();
1231
1125
  const entry = event.compactionEntry;
1232
1126
  recorder?.finishCompaction({
1233
1127
  reason: event.reason,
@@ -1240,39 +1134,102 @@ function createLangfuseExtension(dependencies = {}) {
1240
1134
  if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
1241
1135
  });
1242
1136
  pi.on("agent_settled", (_event, ctx) => {
1243
- recorder?.settle(contextSnapshot(ctx));
1137
+ const recorder = activeRecorder();
1138
+ if (!recorder) return;
1139
+ lastSnapshot = contextSnapshot(ctx);
1140
+ recorder.settle(lastSnapshot);
1141
+ pendingProviderGeneration = void 0;
1244
1142
  nextAttemptReason = void 0;
1245
1143
  });
1246
1144
  pi.on("session_shutdown", async (event, ctx) => {
1145
+ if (ownerRegistration !== registration) return;
1146
+ const initializations = invalidateAllInitializations(event.reason);
1147
+ ownerRegistration = void 0;
1247
1148
  const generation = ++sessionGeneration;
1248
- menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
1249
- const activeRecorder = recorder;
1250
- const shutdownConfig = activeConfig;
1251
- const pendingConfigWrite = configWriteQueue;
1252
- const shutdownSnapshot = activeRecorder ? contextSnapshot(ctx) : void 0;
1253
- recorder = void 0;
1254
- activeConfig = void 0;
1255
- await pendingConfigWrite;
1256
- if (!activeRecorder) return;
1149
+ options.onSessionShutdown?.();
1150
+ const active = binding?.registration === registration ? binding : void 0;
1151
+ const runtime = active?.runtime ?? runtimeForShutdown;
1152
+ lastSnapshot = active ? contextSnapshot(ctx) : lastSnapshot;
1153
+ closeBinding(
1154
+ active,
1155
+ event.reason === "quit" ? "Pi shut down before the active trace settled." : `Pi session ended before settlement (${event.reason}).`,
1156
+ lastSnapshot
1157
+ );
1158
+ let beforeDisposeFailure;
1159
+ try {
1160
+ await options.beforeSessionDispose?.();
1161
+ } catch (error) {
1162
+ beforeDisposeFailure = { error };
1163
+ }
1257
1164
  try {
1258
- if (event.reason === "quit") await activeRecorder.shutdown(shutdownSnapshot);
1259
- else {
1260
- activeRecorder.interrupt(`Pi session ended before settlement (${event.reason}).`, shutdownSnapshot);
1261
- await activeRecorder.flush();
1165
+ await joinInitializations(initializations);
1166
+ if (disposed || generation !== sessionGeneration) return;
1167
+ if (beforeDisposeFailure) throw beforeDisposeFailure.error;
1168
+ if (runtime) {
1169
+ if (event.reason === "quit" && options.shutdownRuntimeOnQuit) await runtime.shutdown();
1170
+ else if (options.flushOnReplacement) await runtime.flush();
1262
1171
  }
1263
1172
  } catch (error) {
1264
- if (generation !== sessionGeneration) return;
1265
- ctx.ui.notify(`Langfuse shutdown export failed: ${formatError2(error, shutdownConfig)}`, "error");
1173
+ if (!disposed && generation === sessionGeneration) options.onShutdownError?.(error, ctx);
1266
1174
  }
1267
1175
  });
1176
+ }
1177
+ function claimPendingProviderGeneration(ctx, recorder) {
1178
+ const pending = pendingProviderGeneration;
1179
+ if (!pending || !recorder) return;
1180
+ pendingProviderGeneration = void 0;
1181
+ ensureActiveRun(recorder, ctx, startRootTrace);
1182
+ recorder.beginGeneration({
1183
+ startedAt: pending.startedAt,
1184
+ payload: pending.payload,
1185
+ payloadStage: "before_provider_request",
1186
+ model: pending.model,
1187
+ thinkingLevel: pending.thinkingLevel
1188
+ });
1189
+ for (const response of pending.responses) recorder.recordProviderResponse(response.status, response.headers);
1190
+ }
1191
+ function startRootTrace(start) {
1192
+ const revision = requestIdRevision;
1193
+ const nextRequestId = requestId;
1194
+ start(nextRequestId);
1195
+ if (requestIdRevision === revision) requestId = void 0;
1196
+ }
1197
+ function activeRecorderFor(registration) {
1198
+ return ownerRegistration === registration && binding?.registration === registration && !binding.runtime.closed ? binding.recorder : void 0;
1199
+ }
1200
+ return controller;
1201
+ }
1202
+ function createRecorderOptions(ctx, options = {}) {
1203
+ const userId = normalizeUserId(options.userId);
1204
+ return {
1205
+ sessionId: normalizeOptionalString(options.sessionId) ?? ctx.sessionManager.getSessionId(),
1206
+ ...userId ? { userId } : {},
1207
+ cwd: ctx.cwd,
1208
+ mode: ctx.mode,
1209
+ captureContent: options.captureContent ?? true,
1210
+ ...normalizeOptionalString(options.traceName) ? { traceName: normalizeOptionalString(options.traceName) } : {},
1211
+ ...options.tags ? { tags: [...options.tags] } : {},
1212
+ ...options.metadata ? { metadata: { ...options.metadata } } : {},
1213
+ ...options.onTraceId ? { onTraceId: options.onTraceId } : {}
1214
+ };
1215
+ }
1216
+ function snapshotSessionOptions(options) {
1217
+ normalizeUserId(options.userId);
1218
+ return {
1219
+ ...options,
1220
+ ...options.tags ? { tags: [...options.tags] } : {},
1221
+ ...options.metadata ? { metadata: { ...options.metadata } } : {}
1268
1222
  };
1269
1223
  }
1270
- function ensureActiveRun(recorder, ctx) {
1224
+ function ensureActiveRun(recorder, ctx, startRootTrace) {
1271
1225
  if (!recorder.hasActiveTrace()) {
1272
- recorder.beginAgent({
1273
- prompt: "[automatic continuation]",
1274
- model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
1275
- snapshot: contextSnapshot(ctx)
1226
+ startRootTrace((nextRequestId) => {
1227
+ recorder.beginAgent({
1228
+ prompt: "[automatic continuation]",
1229
+ model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
1230
+ snapshot: contextSnapshot(ctx),
1231
+ ...nextRequestId ? { requestId: nextRequestId } : {}
1232
+ });
1276
1233
  });
1277
1234
  }
1278
1235
  if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
@@ -1293,56 +1250,200 @@ function findLastAssistant(messages) {
1293
1250
  function isRealOutputDelta(event) {
1294
1251
  return (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") && typeof event.delta === "string" && event.delta.length > 0;
1295
1252
  }
1296
- var GIT_LOOKUP_TIMEOUT_MS = 1e3;
1297
- var MAX_GIT_BRANCH_LENGTH = 256;
1298
- async function resolveGitMetadata(exec, cwd) {
1299
- const [branchResult, commit] = await Promise.all([resolveGitBranch(exec, cwd), resolveGitCommit(exec, cwd)]);
1300
- if (!branchResult.resolved) return void 0;
1301
- if (branchResult.branch) {
1302
- return {
1303
- branch: branchResult.branch,
1304
- ...commit ? { commit } : {},
1305
- detached: false
1306
- };
1307
- }
1308
- return commit ? { commit, detached: true } : void 0;
1253
+ function normalizeUserId(value) {
1254
+ const userId = normalizeOptionalString(value);
1255
+ if (userId && userId.length > 200) throw new Error("Langfuse userId must be at most 200 characters.");
1256
+ return userId;
1309
1257
  }
1310
- async function resolveGitBranch(exec, cwd) {
1311
- try {
1312
- const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
1313
- cwd,
1314
- timeout: GIT_LOOKUP_TIMEOUT_MS
1315
- });
1316
- if (result.killed) return { resolved: false };
1317
- if (result.code === 1) return { resolved: true };
1318
- if (result.code !== 0) return { resolved: false };
1319
- const branch = normalizeGitBranch(result.stdout);
1320
- return branch ? { resolved: true, branch } : { resolved: false };
1321
- } catch {
1322
- return { resolved: false };
1323
- }
1258
+ function normalizeOptionalString(value) {
1259
+ if (typeof value !== "string" || !value.trim()) return void 0;
1260
+ const sanitized = sanitizeTraceValue(value.trim());
1261
+ return typeof sanitized === "string" && sanitized.trim() ? sanitized.trim() : void 0;
1324
1262
  }
1325
- async function resolveGitCommit(exec, cwd) {
1326
- try {
1327
- const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
1328
- cwd,
1329
- timeout: GIT_LOOKUP_TIMEOUT_MS
1263
+
1264
+ // src/langfuse.ts
1265
+ var FLUSH_ACTION = "Flush completed traces for this session";
1266
+ var SET_UP_ACTION = "Set up Langfuse for this Pi agent directory (restart required)";
1267
+ var UPDATE_ACTION = "Update Langfuse for this Pi agent directory (restart required)";
1268
+ var HELP_ACTION = "Show setup and privacy help";
1269
+ function createLangfuseExtension(dependencies = {}) {
1270
+ const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
1271
+ const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
1272
+ const resolveInjectedGitMetadata = dependencies.resolveGitMetadata;
1273
+ return function langfuse(pi) {
1274
+ let activeConfig;
1275
+ let loadingConfig;
1276
+ let runtimeConfigForShutdown;
1277
+ let shutdownConfig;
1278
+ let configPath;
1279
+ let initializationError;
1280
+ let hasStoredConfig = false;
1281
+ let configurationNotice;
1282
+ let sessionGeneration = 0;
1283
+ let menuController = new AbortController();
1284
+ let configWriteQueue = Promise.resolve();
1285
+ const tracing = createPiLangfuseSessionController({
1286
+ onSessionStart() {
1287
+ sessionGeneration += 1;
1288
+ menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
1289
+ menuController = new AbortController();
1290
+ activeConfig = void 0;
1291
+ loadingConfig = void 0;
1292
+ shutdownConfig = void 0;
1293
+ configPath = void 0;
1294
+ initializationError = void 0;
1295
+ hasStoredConfig = false;
1296
+ configurationNotice = void 0;
1297
+ },
1298
+ onSessionShutdown() {
1299
+ sessionGeneration += 1;
1300
+ menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
1301
+ shutdownConfig = activeConfig ?? runtimeConfigForShutdown ?? loadingConfig;
1302
+ activeConfig = void 0;
1303
+ },
1304
+ async resolveSession(ctx, isCurrent) {
1305
+ await configWriteQueue;
1306
+ if (!isCurrent()) return void 0;
1307
+ const result = await loadConfig();
1308
+ if (!isCurrent()) return void 0;
1309
+ configPath = result.path;
1310
+ for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
1311
+ if (!result.ok) {
1312
+ initializationError = formatConfigError(result);
1313
+ ctx.ui.notify(initializationError, "warning");
1314
+ return void 0;
1315
+ }
1316
+ hasStoredConfig = true;
1317
+ loadingConfig = result.config;
1318
+ const createBackend = dependencies.createBackend;
1319
+ const ownsRuntime = createBackend !== void 0;
1320
+ const runtime = createBackend ? createLangfuseRuntimeFromBackend(await createBackend(result.config)) : await (await import("./chunks/runtime-6ZHLVC3A.ts")).createLangfuseRuntime({ config: result.config, env: false });
1321
+ if (isCurrent() || !runtimeConfigForShutdown) runtimeConfigForShutdown = result.config;
1322
+ if (isCurrent()) {
1323
+ activeConfig = result.config;
1324
+ loadingConfig = void 0;
1325
+ }
1326
+ return {
1327
+ runtime,
1328
+ releaseIfStale: async (reason) => {
1329
+ if (ownsRuntime || reason === "quit") await runtime.shutdown();
1330
+ },
1331
+ options: {
1332
+ ...result.config.userId ? { userId: result.config.userId } : {},
1333
+ captureContent: result.config.captureContent
1334
+ }
1335
+ };
1336
+ },
1337
+ ...resolveInjectedGitMetadata ? { resolveGitMetadata: (_pi, cwd) => resolveInjectedGitMetadata(cwd) } : {},
1338
+ onInitializationError(error, ctx) {
1339
+ const config = loadingConfig;
1340
+ loadingConfig = void 0;
1341
+ initializationError = `Langfuse tracing could not start: ${formatError(error, config)}`;
1342
+ ctx.ui.notify(initializationError, "warning");
1343
+ },
1344
+ beforeSessionDispose: async () => {
1345
+ await configWriteQueue;
1346
+ },
1347
+ onShutdownError(error, ctx) {
1348
+ ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`, "error");
1349
+ },
1350
+ flushOnReplacement: true,
1351
+ shutdownRuntimeOnQuit: true
1330
1352
  });
1331
- if (result.code !== 0 || result.killed) return void 0;
1332
- const commit = result.stdout.trim();
1333
- return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : void 0;
1334
- } catch {
1335
- return void 0;
1336
- }
1337
- }
1338
- function normalizeGitBranch(value) {
1339
- const branch = value.trim();
1340
- if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return void 0;
1341
- for (const character of branch) {
1342
- const code = character.codePointAt(0) ?? 0;
1343
- if (code <= 31 || code === 127) return void 0;
1344
- }
1345
- return branch;
1353
+ async function showLangfuseMenu(ctx) {
1354
+ if (!ctx.hasUI) {
1355
+ ctx.ui.notify(formatNonInteractiveStatus(activeConfig, configPath, initializationError), "warning");
1356
+ return;
1357
+ }
1358
+ const menuGeneration = sessionGeneration;
1359
+ const menuSignal = menuController.signal;
1360
+ const isCurrent = () => menuGeneration === sessionGeneration && !menuSignal.aborted;
1361
+ const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
1362
+ if (!isCurrent()) return;
1363
+ const menu = defineMenu({
1364
+ start: "main",
1365
+ screens: {
1366
+ main: () => ({
1367
+ kind: "actions",
1368
+ title: "Langfuse",
1369
+ lines: formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice).split("\n").slice(1),
1370
+ items: [
1371
+ ...tracing.active ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" }] : [],
1372
+ {
1373
+ id: "configure",
1374
+ label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
1375
+ action: "configure"
1376
+ },
1377
+ { id: "help", label: HELP_ACTION, action: "help" }
1378
+ ],
1379
+ hint: "close"
1380
+ })
1381
+ },
1382
+ actions: {
1383
+ flush: async () => {
1384
+ const menuConfig = activeConfig;
1385
+ if (!tracing.active) {
1386
+ ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
1387
+ return { kind: "close" };
1388
+ }
1389
+ try {
1390
+ await tracing.flush();
1391
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
1392
+ ctx.ui.notify("Langfuse traces flushed for this session.", "info");
1393
+ } catch (error) {
1394
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
1395
+ ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
1396
+ }
1397
+ return { kind: "close" };
1398
+ },
1399
+ configure: async () => {
1400
+ const loaded = await loadConfig(configPath);
1401
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
1402
+ configPath = loaded.path;
1403
+ const next = await promptForConfig(
1404
+ ctx,
1405
+ loaded.ok ? loaded.config : void 0,
1406
+ () => menuGeneration === sessionGeneration
1407
+ );
1408
+ if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
1409
+ try {
1410
+ const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
1411
+ configWriteQueue = write.then(
1412
+ () => void 0,
1413
+ () => void 0
1414
+ );
1415
+ await write;
1416
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
1417
+ hasStoredConfig = true;
1418
+ configurationNotice = "Saved; restart each Pi process to use it in subsequent sessions.";
1419
+ ctx.ui.notify(
1420
+ `Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
1421
+ "info"
1422
+ );
1423
+ } catch (error) {
1424
+ if (menuGeneration !== sessionGeneration) return { kind: "close" };
1425
+ ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
1426
+ }
1427
+ return { kind: "close" };
1428
+ },
1429
+ help: async () => {
1430
+ ctx.ui.notify(formatHelp(configPath), "info");
1431
+ return { kind: "close" };
1432
+ }
1433
+ }
1434
+ });
1435
+ await runMenu(ctx, menu, {
1436
+ getState: () => void 0,
1437
+ signal: menuSignal,
1438
+ isCurrent
1439
+ });
1440
+ }
1441
+ pi.registerCommand("langfuse", {
1442
+ description: "Open interactive Langfuse tracing controls",
1443
+ handler: async (_args, ctx) => showLangfuseMenu(ctx)
1444
+ });
1445
+ tracing.extension(pi);
1446
+ };
1346
1447
  }
1347
1448
  async function promptForConfig(ctx, current, isCurrent) {
1348
1449
  const secretKey = await ctx.ui.input("Langfuse secret key (leave blank to keep existing):");
@@ -1427,7 +1528,7 @@ function formatConfigError(result) {
1427
1528
  const setupHint = result.reason.startsWith("Configuration file not found:") ? " Run /langfuse and choose Set up Langfuse." : "";
1428
1529
  return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
1429
1530
  }
1430
- function formatError2(error, config) {
1531
+ function formatError(error, config) {
1431
1532
  let message = error instanceof Error ? error.message : String(error);
1432
1533
  const secrets = [config?.publicKey, config?.secretKey].filter((secret) => Boolean(secret)).sort((left, right) => right.length - left.length);
1433
1534
  for (const secret of secrets) {
@@ -1436,7 +1537,15 @@ function formatError2(error, config) {
1436
1537
  return message;
1437
1538
  }
1438
1539
  var langfuse_default = createLangfuseExtension();
1540
+
1541
+ // src/public-api.ts
1542
+ async function createLangfuseRuntime(options = {}) {
1543
+ const runtime = await import("./chunks/runtime-6ZHLVC3A.ts");
1544
+ return runtime.createLangfuseRuntime(options);
1545
+ }
1439
1546
  export {
1547
+ createLangfuseRuntime,
1548
+ createPiLangfuseSession,
1440
1549
  langfuse_default as default
1441
1550
  };
1442
1551
  //# sourceMappingURL=index.ts.map