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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user