chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -2,20 +2,40 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { fauxAssistantMessage, fauxToolCall, type Model } from "@earendil-works/pi-ai";
import {
fauxAssistantMessage,
fauxToolCall,
getCurrentSystemPrompt,
type Model,
type TranscriptContext,
} from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import type { ExtensionAPI, InputEvent } from "../../src/core/extensions/index.ts";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { BeforeAgentStartEvent, ExtensionAPI, InputEvent } from "../../src/core/extensions/index.ts";
import type { PromptTemplate } from "../../src/core/prompt-templates.ts";
import { createSyntheticSourceInfo } from "../../src/core/source-info.ts";
import { createTestResourceLoader } from "../utilities.ts";
import { createHarness, getMessageText, type Harness } from "./harness.ts";
const processImage = vi.hoisted(() =>
vi.fn(async (_bytes: Uint8Array, mimeType: string) => ({
ok: true as const,
data: Buffer.from("normalized").toString("base64"),
mimeType,
hints: [],
})),
);
vi.mock("../../src/utils/image-process.ts", () => ({ processImage }));
const TINY_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
describe("AgentSession prompt characterization", () => {
const harnesses: Harness[] = [];
const tempDirs: string[] = [];
afterEach(() => {
processImage.mockClear();
while (harnesses.length > 0) {
harnesses.pop()?.cleanup();
}
@@ -35,8 +55,8 @@ describe("AgentSession prompt characterization", () => {
await harness.session.prompt("hi");
expect(harness.session.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
expect(getMessageText(harness.session.messages[0]!)).toBe("hi");
expect(harness.session.messages.map((message) => message.role)).toEqual(["system", "user", "assistant"]);
expect(getMessageText(harness.session.messages[1]!)).toBe("hi");
expect(harness.getPendingResponseCount()).toBe(0);
});
@@ -68,13 +88,14 @@ describe("AgentSession prompt characterization", () => {
expect(toolRuns).toEqual(["hello"]);
expect(harness.session.messages.map((message) => message.role)).toEqual([
"system",
"user",
"assistant",
"toolResult",
"assistant",
]);
expect(harness.session.messages[2]?.role).toBe("toolResult");
expect(harness.session.messages[3]?.role).toBe("assistant");
expect(harness.session.messages[3]?.role).toBe("toolResult");
expect(harness.session.messages[4]?.role).toBe("assistant");
});
it("executes multiple tool calls from one response and continues with a single follow-up response", async () => {
@@ -144,6 +165,44 @@ describe("AgentSession prompt characterization", () => {
expect(sawImage).toBe(true);
});
// Regression test for https://github.com/earendil-works/pi/issues/9631
it("uses the model selected by before_agent_start for image normalization", async () => {
let strictModel: Model<string> | undefined;
const harness = await createHarness({
models: [{ id: "wide" }, { id: "strict" }],
extensionFactories: [
(pi) => {
pi.on("before_agent_start", async () => {
if (!strictModel) throw new Error("Expected strict model");
await pi.setModel(strictModel);
});
},
],
});
harnesses.push(harness);
strictModel = harness.getModel("strict");
if (!strictModel) throw new Error("Expected strict model");
const resizeOptions = { maxWidth: 1000, maxHeight: 1000, maxBytes: 500000, jpegQuality: 70 };
strictModel.inputLimits = { images: { resize: resizeOptions } };
harness.setResponses([fauxAssistantMessage("done")]);
await harness.session.prompt("inspect", {
images: [{ type: "image", data: TINY_PNG_BASE64, mimeType: "image/png" }],
});
expect(harness.session.model?.id).toBe("strict");
expect(processImage).toHaveBeenCalledWith(expect.any(Uint8Array), "image/png", {
autoResizeImages: true,
resizeOptions,
});
const userMessage = harness.session.messages.find((message) => message.role === "user");
expect(userMessage?.content).toContainEqual({
type: "image",
data: Buffer.from("normalized").toString("base64"),
mimeType: "image/png",
});
});
it("expands skill commands before sending the prompt", async () => {
const tempDir = join(tmpdir(), `pi-skill-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(tempDir, { recursive: true });
@@ -318,8 +377,8 @@ describe("AgentSession prompt characterization", () => {
await harness.session.sendUserMessage("from extension");
expect(harness.session.messages.map((message) => message.role)).toEqual(["user", "assistant"]);
expect(getMessageText(harness.session.messages[0]!)).toBe("from extension");
expect(harness.session.messages.map((message) => message.role)).toEqual(["system", "user", "assistant"]);
expect(getMessageText(harness.session.messages[1]!)).toBe("from extension");
});
it("does not report streamingBehavior to input handlers while idle", async () => {
@@ -503,4 +562,162 @@ describe("AgentSession prompt characterization", () => {
`No API key found for ${harness.getModel().provider}.`,
);
});
// Issue #10267, #5581: a run started by sendMessage(..., { triggerTurn: true }) skipped
// before_agent_start, so its second request patched away the sections handlers had added.
describe("runs started by a custom message", () => {
it("keeps the same system prompt for every request of the run", async () => {
const events: BeforeAgentStartEvent[] = [];
const noopTool: AgentTool = {
name: "noop",
label: "Noop",
description: "Does nothing",
parameters: Type.Object({}),
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
};
const harness = await createHarness({
tools: [noopTool],
extensionFactories: [
(pi) => {
pi.on("before_agent_start", (event) => {
events.push(event);
event.systemPromptOptions.sections.extension_rules = "Answer in one word.";
});
},
],
});
harnesses.push(harness);
const prompts: string[] = [];
const record = (response: ReturnType<typeof fauxAssistantMessage>) => (context: TranscriptContext) => {
prompts.push(getCurrentSystemPrompt(context.messages));
return response;
};
harness.setResponses([
record(fauxAssistantMessage("hi")),
record(fauxAssistantMessage([fauxToolCall("noop", {})], { stopReason: "toolUse" })),
record(fauxAssistantMessage("done")),
]);
await harness.session.prompt("hello");
await harness.session.sendCustomMessage(
{ customType: "task-done", content: "background task finished", display: true },
{ triggerTurn: true },
);
expect(prompts).toHaveLength(3);
expect(prompts[0]).toContain("Answer in one word.");
expect(new Set(prompts).size).toBe(1);
expect(events.map((event) => event.messageType)).toEqual(["user", "custom"]);
const customEvent = events[1];
expect(customEvent.messageType === "custom" && customEvent.message.customType).toBe("task-done");
});
it("queues a custom message sent while before_agent_start runs into the starting run", async () => {
let releaseHandler: () => void = () => {};
const handlerStarted = Promise.withResolvers<void>();
const harness = await createHarness({
extensionFactories: [
(pi) => {
pi.on("before_agent_start", async (event) => {
if (event.messageType !== "custom" || event.message.customType !== "first") return;
handlerStarted.resolve();
await new Promise<void>((resolve) => {
releaseHandler = resolve;
});
});
},
],
});
harnesses.push(harness);
const requests: TranscriptContext[] = [];
harness.setResponses([
(context) => {
requests.push(context);
return fauxAssistantMessage("handled both");
},
]);
const first = harness.session.sendCustomMessage(
{ customType: "first", content: "task one finished", display: true },
{ triggerTurn: true },
);
await handlerStarted.promise;
expect(harness.session.isIdle).toBe(false);
const second = harness.session.sendCustomMessage(
{ customType: "second", content: "task two finished", display: true },
{ triggerTurn: true },
);
releaseHandler();
await Promise.all([first, second]);
expect(requests).toHaveLength(1);
const texts = requests[0].messages.flatMap((message) =>
message.role === "user" && typeof message.content !== "string"
? message.content.flatMap((part) => (part.type === "text" ? [part.text] : []))
: [],
);
expect(texts).toEqual(["task one finished", "task two finished"]);
});
it("includes a custom message sent without triggerTurn while the run starts in its first request", async () => {
const harness = await createHarness({
extensionFactories: [
(pi) => {
pi.on("before_agent_start", () => {
pi.sendMessage(
{ customType: "note", content: "context from handler", display: true },
{ triggerTurn: false },
);
});
},
],
});
harnesses.push(harness);
const requests: TranscriptContext[] = [];
harness.setResponses([
(context) => {
requests.push(context);
return fauxAssistantMessage("done");
},
]);
await harness.session.prompt("hello");
expect(requests).toHaveLength(1);
expect(JSON.stringify(requests[0].messages)).toContain("context from handler");
});
it("aborts a run when abort is requested while before_agent_start runs", async () => {
let releaseHandler: () => void = () => {};
const handlerStarted = Promise.withResolvers<void>();
const harness = await createHarness({
extensionFactories: [
(pi) => {
pi.on("before_agent_start", async () => {
handlerStarted.resolve();
await new Promise<void>((resolve) => {
releaseHandler = resolve;
});
});
},
],
});
harnesses.push(harness);
harness.setResponses([fauxAssistantMessage("should not complete")]);
const run = harness.session.sendCustomMessage(
{ customType: "task-done", content: "background task finished", display: true },
{ triggerTurn: true },
);
await handlerStarted.promise;
const aborted = harness.session.abort();
releaseHandler();
await Promise.all([run, aborted]);
const last = harness.session.messages.at(-1);
expect(last?.role).toBe("assistant");
expect(last?.role === "assistant" && last.stopReason).toBe("aborted");
expect(harness.session.isIdle).toBe(true);
});
});
});