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

@@ -4,8 +4,7 @@ import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import {
type AssistantMessage,
type AssistantMessageEvent,
EventStream,
createAssistantMessageEventStream,
getModel,
type Model,
} from "@earendil-works/pi-ai/compat";
@@ -13,11 +12,12 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { AgentSession } from "../src/core/agent-session.ts";
import type { AgentSessionRuntime } from "../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import type { LoadExtensionsResult } from "../src/core/extensions/index.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
import { runRpcMode } from "../src/modes/rpc/rpc-mode.ts";
import { createInMemoryModelRegistry, getModelRuntime } from "./model-runtime-test-utils.ts";
import { createTestResourceLoader } from "./utilities.ts";
import { createTestExtensionsResult, createTestResourceLoader } from "./utilities.ts";
const rpcIo = vi.hoisted(() => ({
outputLines: [] as string[],
@@ -43,19 +43,6 @@ vi.mock("../src/modes/rpc/jsonl.js", () => ({
serializeJsonLine: (value: unknown) => `${JSON.stringify(value)}\n`,
}));
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
constructor() {
super(
(event) => event.type === "done" || event.type === "error",
(event) => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
},
);
}
}
function createAssistantMessage(text: string): AssistantMessage {
return {
role: "assistant",
@@ -95,7 +82,12 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs: number; model?: Model<any> }): Promise<{
async function createRuntimeHost(options: {
withAuth: boolean;
responseDelayMs: number;
model?: Model<any>;
extensionsResult?: LoadExtensionsResult;
}): Promise<{
runtimeHost: AgentSessionRuntime;
cleanup: () => Promise<void>;
}> {
@@ -115,7 +107,7 @@ async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs:
tools: [],
},
streamFn: (_model, _context, _options) => {
const stream = new MockAssistantStream();
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
stream.push({ type: "start", partial: createAssistantMessage("") });
setTimeout(() => {
@@ -140,7 +132,7 @@ async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs:
settingsManager,
cwd: tempDir,
modelRuntime: getModelRuntime(modelRegistry),
resourceLoader: createTestResourceLoader(),
resourceLoader: createTestResourceLoader({ extensionsResult: options.extensionsResult }),
});
const runtimeHost = {
@@ -168,7 +160,7 @@ async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs:
};
}
async function startRpcMode(options: { withAuth: boolean; responseDelayMs: number; model?: Model<any> }): Promise<{
async function startRpcMode(options: Parameters<typeof createRuntimeHost>[0]): Promise<{
lineHandler: (line: string) => void;
cleanup: () => Promise<void>;
}> {
@@ -227,7 +219,8 @@ describe("RPC prompt response semantics", () => {
}
});
it("emits one success response when prompt preflight succeeds", async () => {
// #9098: a successful prompt may start an agent run or be consumed by an extension.
it("emits one started response when prompt preflight succeeds", async () => {
const { lineHandler, cleanup } = await startRpcMode({ withAuth: true, responseDelayMs: 0 });
try {
@@ -241,6 +234,7 @@ describe("RPC prompt response semantics", () => {
type: "response",
command: "prompt",
success: true,
data: { disposition: "started" },
});
});
} finally {
@@ -248,6 +242,38 @@ describe("RPC prompt response semantics", () => {
}
});
it("reports extension commands and intercepted input as handled without starting a run", async () => {
const { lineHandler, cleanup } = await startRpcMode({
withAuth: false,
responseDelayMs: 0,
extensionsResult: await createTestExtensionsResult([
(pi) => {
pi.registerCommand("handled", { handler: async () => {} });
pi.on("input", (event) => {
if (event.text === "handled input") return { action: "handled" };
});
},
]),
});
try {
for (const [id, message] of [
["command", "/handled"],
["input", "handled input"],
]) {
lineHandler(JSON.stringify({ id, type: "prompt", message }));
await vi.waitFor(() => {
expect(getPromptResponses(rpcIo.outputLines, id)).toEqual([
{ id, type: "response", command: "prompt", success: true, data: { disposition: "handled" } },
]);
});
}
expect(parseOutputLines(rpcIo.outputLines).filter((line) => line.type === "agent_start")).toHaveLength(0);
} finally {
await cleanup();
}
});
it("emits one success response when prompt is queued during streaming", async () => {
const { lineHandler, cleanup } = await startRpcMode({ withAuth: true, responseDelayMs: 100 });
@@ -275,6 +301,7 @@ describe("RPC prompt response semantics", () => {
type: "response",
command: "prompt",
success: true,
data: { disposition: "queued" },
});
});
@@ -284,6 +311,89 @@ describe("RPC prompt response semantics", () => {
}
});
// #9803: a handler can consume A while independently queueing B; report A's outcome.
it.each(["steer", "follow_up"] as const)(
"reports %s as handled even when an extension queues another message",
async (type) => {
const { lineHandler, cleanup } = await startRpcMode({
withAuth: true,
responseDelayMs: 500,
extensionsResult: await createTestExtensionsResult([
(pi) => {
pi.on("input", (event) => {
if (event.text === "A" && event.source === "rpc") {
pi.sendUserMessage("B", { deliverAs: type === "steer" ? "steer" : "followUp" });
return { action: "handled" };
}
});
},
]),
});
try {
lineHandler(JSON.stringify({ id: "start", type: "prompt", message: "Start" }));
await vi.waitFor(() => expect(getPromptResponses(rpcIo.outputLines, "start")).toHaveLength(1));
lineHandler(JSON.stringify({ id: "A", type, message: "A" }));
await vi.waitFor(() => {
expect(parseOutputLines(rpcIo.outputLines)).toContainEqual({
id: "A",
type: "response",
command: type,
success: true,
data: { disposition: "handled" },
});
expect(parseOutputLines(rpcIo.outputLines)).toContainEqual({
type: "queue_update",
steering: type === "steer" ? ["B"] : [],
followUp: type === "follow_up" ? ["B"] : [],
});
});
await vi.waitFor(() => {
expect(
parseOutputLines(rpcIo.outputLines).filter((line) => line.type === "response" && line.id === "A"),
).toHaveLength(1);
});
} finally {
await cleanup();
}
},
);
it.each(["steer", "follow_up"] as const)("reports %s as queued after input transformation", async (type) => {
const { lineHandler, cleanup } = await startRpcMode({
withAuth: false,
responseDelayMs: 0,
extensionsResult: await createTestExtensionsResult([
(pi) => {
pi.on("input", (event) => {
if (event.text === "A") return { action: "transform", text: "B" };
});
},
]),
});
try {
lineHandler(JSON.stringify({ id: "A", type, message: "A" }));
await vi.waitFor(() => {
expect(parseOutputLines(rpcIo.outputLines)).toContainEqual({
id: "A",
type: "response",
command: type,
success: true,
data: { disposition: "queued" },
});
expect(parseOutputLines(rpcIo.outputLines)).toContainEqual({
type: "queue_update",
steering: type === "steer" ? ["B"] : [],
followUp: type === "follow_up" ? ["B"] : [],
});
});
} finally {
await cleanup();
}
});
it("returns and clears queued steering and follow-up messages", async () => {
const { lineHandler, cleanup } = await startRpcMode({ withAuth: true, responseDelayMs: 500 });
@@ -302,7 +412,9 @@ describe("RPC prompt response semantics", () => {
}),
);
await vi.waitFor(() => {
expect(getPromptResponses(rpcIo.outputLines, "clear-steering")).toHaveLength(1);
expect(getPromptResponses(rpcIo.outputLines, "clear-steering")).toMatchObject([
{ data: { disposition: "queued" } },
]);
});
lineHandler(