@braintrust/pi-extension 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @braintrust/pi-extension
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/%40braintrust%2Fpi-extension)](https://www.npmjs.com/package/@braintrust/pi-extension)
4
+
3
5
  Braintrust extension for [pi](https://github.com/mariozechner/pi-coding-agent).
4
6
 
5
7
  Today this extension automatically traces pi sessions, turns, model calls, and tool executions to Braintrust.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braintrust/pi-extension",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Braintrust extension for pi. Includes automatic tracing for pi sessions, turns, LLM calls, and tool executions to Braintrust.",
5
5
  "keywords": [
6
6
  "braintrust",
@@ -24,12 +24,12 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "braintrust": "^3.8.0",
27
+ "braintrust": "^3.9.0",
28
28
  "valibot": "^1.3.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@mariozechner/pi-ai": "^0.67.2",
32
- "@mariozechner/pi-coding-agent": "^0.67.2",
31
+ "@mariozechner/pi-ai": "^0.68.0",
32
+ "@mariozechner/pi-coding-agent": "^0.68.0",
33
33
  "@types/node": "^25.6.0",
34
34
  "typescript": "^6.0.2",
35
35
  "vite-plus": "^0.1.16",
package/src/index.test.ts CHANGED
@@ -411,6 +411,55 @@ describe("braintrustPiExtension", () => {
411
411
  );
412
412
  });
413
413
 
414
+ it("records the structured shutdown reason on the finalized root span", async () => {
415
+ const { emit } = await createHarness();
416
+
417
+ await emit("session_start");
418
+ await emit("before_agent_start", {
419
+ prompt: "Inspect the package",
420
+ images: [],
421
+ });
422
+ await emit("session_shutdown", { reason: "quit" });
423
+
424
+ const rootFinalizeLog = mockState.logSpans
425
+ .map((entry) => entry.event as Record<string, unknown>)
426
+ .find(
427
+ (event) =>
428
+ (event.metadata as Record<string, unknown> | undefined)?.last_close_reason === "quit",
429
+ );
430
+ expect(rootFinalizeLog).toBeDefined();
431
+ expect(mockState.endSpans.length).toBeGreaterThan(0);
432
+ expect(mockState.flushCalls).toBeGreaterThan(0);
433
+ });
434
+
435
+ it("does not finalize the root span on reload shutdowns", async () => {
436
+ const { emit } = await createHarness();
437
+
438
+ await emit("session_start");
439
+ await emit("before_agent_start", {
440
+ prompt: "Inspect the package",
441
+ images: [],
442
+ });
443
+
444
+ const startsBefore = mockState.startSpans.length;
445
+ const endsBefore = mockState.endSpans.length;
446
+ const flushesBefore = mockState.flushCalls;
447
+
448
+ await emit("session_shutdown", { reason: "reload" });
449
+
450
+ // No additional span endings during reload, but pending writes are still flushed.
451
+ expect(mockState.startSpans.length).toBe(startsBefore);
452
+ expect(mockState.endSpans.length).toBe(endsBefore);
453
+ expect(mockState.flushCalls).toBeGreaterThan(flushesBefore);
454
+ const reloadClose = mockState.logSpans
455
+ .map((entry) => entry.event as Record<string, unknown>)
456
+ .some(
457
+ (event) =>
458
+ (event.metadata as Record<string, unknown> | undefined)?.last_close_reason === "reload",
459
+ );
460
+ expect(reloadClose).toBe(false);
461
+ });
462
+
414
463
  it("hides all UI when showUi is false", async () => {
415
464
  mockState.config.showUi = false;
416
465
 
package/src/index.ts CHANGED
@@ -132,7 +132,7 @@ function getPreviousSessionFile(event: unknown): string | undefined {
132
132
  return typeof event.previousSessionFile === "string" ? event.previousSessionFile : undefined;
133
133
  }
134
134
 
135
- function getSessionStartReason(event: unknown): string | undefined {
135
+ function getEventReason(event: unknown): string | undefined {
136
136
  if (!isPlainObject(event)) return undefined;
137
137
  return typeof event.reason === "string" ? event.reason : undefined;
138
138
  }
@@ -606,7 +606,7 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void {
606
606
  pi.on("session_start", async (event, ctx) => {
607
607
  refreshTracingUi(ctx);
608
608
 
609
- const reason = getSessionStartReason(event);
609
+ const reason = getEventReason(event);
610
610
  if (reason === "new" || reason === "resume" || reason === "fork") {
611
611
  await rolloverSession(
612
612
  ctx,
@@ -837,13 +837,27 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void {
837
837
  await finishTurn("agent_end", Date.now(), finalAssistant);
838
838
  });
839
839
 
840
- pi.on("session_shutdown", async (_event, ctx) => {
840
+ pi.on("session_shutdown", async (event, ctx) => {
841
841
  if (ctx.hasUI) {
842
842
  ctx.ui.setStatus(TRACING_STATUS_KEY, undefined);
843
843
  ctx.ui.setWidget(TRACING_WIDGET_KEY, undefined);
844
844
  }
845
+
846
+ // pi 0.68.0+ exposes a structured reason ("quit" | "reload" | "new" | "resume"
847
+ // | "fork"). Older pi hosts pass no payload, so we fall back to the generic
848
+ // label to stay backwards-compatible and keep the existing metadata shape.
849
+ const reason = getEventReason(event) ?? "session_shutdown";
850
+ logger.debug("session_shutdown", { reason });
851
+
845
852
  if (client && !clientInitializationError) {
846
- await finalizeSession("session_shutdown");
853
+ // On reload the same pi session is about to resume in a freshly imported
854
+ // extension instance, which restores its state from the persisted store and
855
+ // keeps writing to the existing root span. Finalizing here would close that
856
+ // root span out from under the reloaded instance, so we just flush pending
857
+ // writes and let the new instance continue the trace.
858
+ if (reason !== "reload") {
859
+ await finalizeSession(reason);
860
+ }
847
861
  await client.flush();
848
862
  }
849
863
  activeSession = undefined;