Skip to content
Open
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 .changeset/pi-coding-agent-stream-function-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": patch
---

fix(pi-coding-agent): Support `agent.streamFunction` rename in `@earendil-works/pi-coding-agent` >= 0.81
42 changes: 40 additions & 2 deletions js/src/instrumentation/plugins/pi-coding-agent-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,41 @@ describe("PiCodingAgentPlugin", () => {
await result;
expect(taskSpan?.end).toHaveBeenCalledTimes(1);
});

it("patches agent.streamFunction when streamFn is absent (pi >= 0.81)", async () => {
const interceptor = promptInterceptor(enablePlugin(plugins));
const finalMessage = makeAssistantMessage("done");
const originalStreamFn = vi.fn(async () => makeStream(finalMessage));
const agent = makeAgent(originalStreamFn, "streamFunction");
Comment on lines +431 to +435

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exercise the renamed SDK in e2e

This synthetic-agent unit test is the only new verification, while the Pi e2e scenario remains pinned to 0.79.1 and 0.79.10, so neither the real 0.81+ package shape nor its wrapped and auto-hook paths are exercised. Add pinned and separately named latest dependency variants for the renamed API and include both in the CI e2e summary as required for newly supported instrumentation versions.

AGENTS.md reference: AGENTS.md:L60-L60

Useful? React with 👍 / 👎.

const session = makeSession(agent);

await interceptor(
async function (this: typeof session) {
const stream = await this.agent.streamFunction(
anthropicModel(),
{ systemPrompt: "system", messages: [], tools: [] },
{},
);
await stream.result();
await this.agent.emit({
message: finalMessage,
toolResults: [],
turnIndex: 0,
type: "turn_end",
});
},
session,
["hello", undefined],
{ moduleVersion: "0.84.2" },
);

expect(agent.streamFunction).not.toBe(originalStreamFn);
expect(agent.streamFn).toBeUndefined();
expect(originalStreamFn).toHaveBeenCalled();

const taskSpan = findSpan(spans, "AgentSession.prompt");
expect(taskSpan?.end).toHaveBeenCalledTimes(1);
});
});

function enablePlugin(plugins: PiCodingAgentPlugin[]): PiCodingAgentPlugin {
Expand Down Expand Up @@ -521,11 +556,14 @@ function makeIteratorBackedStream(events: any[]) {
};
}

function makeAgent(streamFn: any) {
function makeAgent(
streamFn: any,
streamFnKey: "streamFn" | "streamFunction" = "streamFn",
) {
const listeners = new Set<any>();
return {
state: { model: anthropicModel(), tools: [] as any[] },
streamFn,
[streamFnKey]: streamFn,
subscribe: vi.fn((listener) => {
listeners.add(listener);
return vi.fn(() => listeners.delete(listener));
Expand Down
34 changes: 29 additions & 5 deletions js/src/instrumentation/plugins/pi-coding-agent-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type PiToolSpanState = {

type PiAgentPatchState = {
originalStreamFn: PiStreamFn;
streamFnKey: "streamFn" | "streamFunction";
wrappedStreamFn: PiStreamFn;
};

Expand Down Expand Up @@ -228,11 +229,23 @@ function extractSession(
function isPiAgent(value: unknown): value is PiAgent {
return (
isObject(value) &&
typeof value.streamFn === "function" &&
(typeof value.streamFn === "function" ||
typeof value.streamFunction === "function") &&
typeof value.subscribe === "function"
);
}

// @earendil-works/pi-coding-agent renamed `agent.streamFn` to
// `agent.streamFunction` starting in v0.81.0. Resolve whichever property
// actually exists so both the pre- and post-rename shapes are patchable.
Comment on lines +239 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expand the auto-instrumentation version range

For the reported 0.84.2 auto-hook case, this resolver is never reached: js/src/auto-instrumentations/configs/pi-coding-agent.ts:4 still restricts the transform to >=0.79.0 <0.82.0, so Orchestrion skips AgentSession.prompt on every 0.82+ installation. The manual wrapper may benefit, but default auto-instrumentation continues emitting no Pi spans; extend the config and verify the target path for the versions this change claims to support.

AGENTS.md reference: AGENTS.md:L28-L30

Useful? React with 👍 / 👎.

function resolveStreamFnKey(
agent: PiAgent,
): "streamFn" | "streamFunction" | undefined {
if (typeof agent.streamFn === "function") return "streamFn";
if (typeof agent.streamFunction === "function") return "streamFunction";
return undefined;
}

function promptContextStore(): IsoAsyncLocalStorage<PiPromptState | undefined> {
piPromptContextStore ??= iso.newAsyncLocalStorage<
PiPromptState | undefined
Expand All @@ -245,17 +258,28 @@ function currentPiPromptState(): PiPromptState | undefined {
}

function installPiAgentInstrumentation(agent: PiAgent): void {
const streamFnKey = resolveStreamFnKey(agent);
if (!streamFnKey) {
// isPiAgent() already validated that one of these exists; this should be
// unreachable, but stay defensive rather than patching the wrong property.
throw new Error(
"Pi Coding Agent: unable to resolve streamFn/streamFunction property",
);
}

const existing = piAgentPatchStates.get(agent);
if (!existing || agent.streamFn !== existing.wrappedStreamFn) {
if (!existing || agent[streamFnKey] !== existing.wrappedStreamFn) {
const originalStreamFn = agent[streamFnKey];
const patchState = {
originalStreamFn: agent.streamFn,
wrappedStreamFn: agent.streamFn,
originalStreamFn,
streamFnKey,
wrappedStreamFn: originalStreamFn,
} satisfies PiAgentPatchState;
patchState.wrappedStreamFn = makeInstrumentedStreamFn(
agent,
patchState.originalStreamFn,
);
agent.streamFn = patchState.wrappedStreamFn;
agent[streamFnKey] = patchState.wrappedStreamFn;
piAgentPatchStates.set(agent, patchState);
}

Expand Down
6 changes: 5 additions & 1 deletion js/src/vendor-sdk-types/pi-coding-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export interface PiPromptOptions {
}

export interface PiAgent {
streamFn: PiStreamFn;
// Renamed from `streamFn` to `streamFunction` in
// @earendil-works/pi-coding-agent v0.81.0. Both are optional here since only
// one exists on any given installed version.
streamFn?: PiStreamFn;
streamFunction?: PiStreamFn;
subscribe(listener: PiAgentEventListener): () => void;
readonly state?: {
model?: PiModel;
Expand Down
Loading