Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bt-daemon/docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ coding-agent span falls back to Rust SDK provenance. Transcript backfills captur
versions available at materialization, not unrecorded historical versions. See the
[span-origin contract](../../docs/coding-agent-tracing-plugin-spec.md#621-span-origin-versions).

Pi-compatible hosts retain the canonical transport source `pi` so they share its
translator. The Pi extension sends the running host package's version as
`source_version` and its package name as `payload.host_name`. The translator records
`metadata.host_name`; the session-root label remains `Pi` for every Pi-compatible host.

For acknowledged spans from legacy ledgers without saved snapshots, stateless
updates omit `context.span_origin` entirely, including SDK defaults and inherited
parent origin fields. This preserves existing backend provenance without guessing
Expand Down
8 changes: 8 additions & 0 deletions bt-daemon/src/translate/pi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,14 @@ impl PiTranslator {
metadata.insert("model".into(), json!(model));
}
metadata.insert("pi_version".into(), json!(envelope.source_version));
if let Some(host_name) = envelope
.payload
.get("host_name")
.and_then(Value::as_str)
.filter(|name| !name.is_empty())
{
metadata.insert("host_name".into(), json!(host_name));
}
metadata.insert(
"extension_version".into(),
envelope
Expand Down
27 changes: 27 additions & 0 deletions bt-daemon/tests/pi_translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,33 @@ fn reduce(ops: Vec<SpanOp>) -> HashMap<String, SpanRow> {
rows
}

#[test]
fn pi_records_the_running_host_identity() {
let registry = Registry::default_agents();
let mut translator = registry.create("pi", "pi-session");
let ctx = SessionCtx {
session_id: "pi-session".into(),
config: None,
};
let mut start = event("session_start", 1, json!({"reason":"new"}));
start.source_version = Some("18.12.6".into());
start.payload["host_name"] = json!("@oh-my-pi/pi-coding-agent");
let ops = translator.handle(&start, &ctx).unwrap();
let root = ops
.iter()
.find_map(|op| match op {
SpanOp::Insert(row) if row.parent_span_ids.is_empty() => Some(row),
_ => None,
})
.unwrap();
assert_eq!(root.name, "Pi");
assert_eq!(
root.metadata.as_ref().unwrap()["host_name"],
"@oh-my-pi/pi-coding-agent"
);
assert_eq!(root.metadata.as_ref().unwrap()["pi_version"], "18.12.6");
}

#[test]
fn pi_builds_turn_llm_tool_compaction_and_shutdown_spans() {
let registry = Registry::default_agents();
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/pi/content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ from each of the last five stable Pi release lines. The compatibility job
resolves these versions on each run, including releases from Pi's former npm
package name when needed.

Package detection prefers the running CLI to an extension-local peer dependency.
Pi-compatible hosts are recognized by a `piConfig` field or a known package name,
including Oh My Pi (`@oh-my-pi/pi-coding-agent`). The trace records the host
package name in `metadata.host_name` and its version in `metadata.pi_version`.

## Configuration

Settings load in this order, with later values taking precedence:
Expand Down
18 changes: 18 additions & 0 deletions src/plugins/pi/content/src/daemon-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ vi.mock("./legacy-session.ts", () => ({
},
}));

vi.mock("./pi-package.ts", () => ({
runningPiPackage: () => ({
name: "@oh-my-pi/pi-coding-agent",
version: "18.12.6",
configDir: ".pi",
}),
}));

type Handler = (...args: unknown[]) => Promise<unknown>;

/** Load the extension against a fake Pi and return its registered handlers. */
Expand Down Expand Up @@ -112,6 +120,16 @@ describe("Pi daemon adapter", () => {
expect(handlers.size).toBe(0);
});

it("forwards the running host name and version instead of an installed peer", async () => {
const handlers = await loadExtension();
await handlers.get("session_start")?.({ reason: "new" }, fakeContext());
expect(mockState.logs[0]).toMatchObject({
source: "pi",
source_version: "18.12.6",
payload: { host_name: "@oh-my-pi/pi-coding-agent" },
});
});

it("does not block session startup on daemon status", async () => {
const ctx = fakeContext();
const handlers = await loadExtension();
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/pi/content/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const STATUS_KEY = "braintrust-tracing";
const WIDGET_KEY = "braintrust-trace-link";
const UI_STATUS_TIMEOUT_MS = 250;
const PI_VERSION = runningPiPackage().version;
const PI_HOST_NAME = runningPiPackage().name;

function sessionKeyFor(
sessionFile: string | undefined,
Expand Down Expand Up @@ -145,6 +146,7 @@ export default function braintrustPiExtension(pi: ExtensionAPI): void {
payload: {
event: nativePayload(event),
extension_version: EXTENSION_VERSION,
host_name: PI_HOST_NAME,
session_file: descriptor?.sessionFile,
native_session_id: descriptor?.nativeSessionId,
legacy_resume: legacyContinuation,
Expand Down
42 changes: 36 additions & 6 deletions src/plugins/pi/content/src/pi-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("loadPiPackageMetadata", () => {
symlinkSync(installPi(`node_modules/${PI}`, { name: PI, version: "1.0.0" }), bin);
return [bin];
},
{ version: "1.0.0", configDir: ".pi" },
{ name: PI, version: "1.0.0", configDir: ".pi" },
],
[
"a fork with its own name and config directory",
Expand All @@ -53,23 +53,23 @@ describe("loadPiPackageMetadata", () => {
piConfig: { configDir: ".acme" },
}),
],
{ version: "3.2.1", configDir: ".acme" },
{ name: "@acme/agent", version: "3.2.1", configDir: ".acme" },
],
[
"a PI_PACKAGE_DIR override",
() => {
installPi("store/pi", { name: PI, version: "1.0.0" });
return [join(root, "store", "pi", "package.json"), join(root, "store", "bin")];
},
{ version: "1.0.0", configDir: ".pi" },
{ name: PI, version: "1.0.0", configDir: ".pi" },
],
[
"a Bun-compiled binary with package.json beside it",
() => {
writeJson(join(root, "pi-linux-x64", "package.json"), { name: PI, version: "1.0.0" });
return [undefined, "/$bunfs/root/pi", join(root, "pi-linux-x64", "pi")];
},
{ version: "1.0.0", configDir: ".pi" },
{ name: PI, version: "1.0.0", configDir: ".pi" },
],
[
"an SDK application that depends on Pi",
Expand All @@ -78,17 +78,47 @@ describe("loadPiPackageMetadata", () => {
const pi = installPi(`app/node_modules/${PI}`, { name: PI, version: "0.99.2" });
return [join(root, "app", "main.js"), pi];
},
{ version: "0.99.2", configDir: ".pi" },
{ name: PI, version: "0.99.2", configDir: ".pi" },
],
[
"no discoverable Pi",
() => [join(root, "missing.js")],
{ version: undefined, configDir: ".pi" },
{ name: undefined, version: undefined, configDir: ".pi" },
],
])("finds %s", (_name, setup, expected) => {
expect(loadPiPackageMetadata(setup())).toEqual(expected);
});

it("prefers OMP's running manifest over an auto-installed upstream peer", () => {
const host = installPi("omp", {
name: "@oh-my-pi/pi-coding-agent",
version: "18.12.6",
bin: { omp: "dist/cli.js" },
});
const peer = installPi(`plugins/node_modules/${PI}`, { name: PI, version: "1.4.0" });
// Recognizing OMP must not move the extension's existing `.pi` settings.
expect(loadPiPackageMetadata([host, peer])).toEqual({
name: "@oh-my-pi/pi-coding-agent",
version: "18.12.6",
configDir: ".pi",
});
});

it("reports the embedded Pi for an SDK application's own CLI", () => {
const app = installPi("app", {
name: "acme-agent",
version: "0.3.0",
bin: { acme: "dist/cli.js" },
dependencies: { [PI]: "^1.4.0" },
});
const pi = installPi(`app/node_modules/${PI}`, { name: PI, version: "1.4.0" });
expect(loadPiPackageMetadata([app, pi])).toEqual({
name: PI,
version: "1.4.0",
configDir: ".pi",
});
});

it("resolves the installed Pi development dependency through ESM conditions", () => {
const metadata = loadPiPackageMetadata([resolvePiPackage()]);
expect(metadata.version).toMatch(/^\d+\.\d+\.\d+/);
Expand Down
8 changes: 7 additions & 1 deletion src/plugins/pi/content/src/pi-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const PI_PACKAGE = "@earendil-works/pi-coding-agent";
const PI_PACKAGE_NAMES = new Set([PI_PACKAGE, "@mariozechner/pi-coding-agent"]);
const PI_PACKAGE_NAMES = new Set([
PI_PACKAGE,
"@mariozechner/pi-coding-agent",
"@oh-my-pi/pi-coding-agent",
]);

interface PiPackageManifest {
name?: unknown;
Expand All @@ -15,6 +19,7 @@ interface PiPackageManifest {
}

export interface PiPackageMetadata {
name?: string;
version?: string;
configDir: string;
}
Expand All @@ -37,6 +42,7 @@ export function loadPiPackageMetadata(candidates: (string | undefined)[]): PiPac
if (manifest) break;
}
return {
name: typeof manifest?.name === "string" ? manifest.name : undefined,
version: typeof manifest?.version === "string" ? manifest.version : undefined,
configDir:
typeof manifest?.piConfig?.configDir === "string" ? manifest.piConfig.configDir : ".pi",
Expand Down
Loading