Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import type { AgentRuntimeEvent, ConnectorTool } from "@rakazo/adapter-kit";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const fakeAgentState = vi.hoisted(() => ({
|
|
options: ["Berlin", "Seoul", "Toronto"],
|
|
tools: [] as Array<{
|
|
name: string;
|
|
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
}>,
|
|
}));
|
|
|
|
vi.mock("@earendil-works/pi-agent-core", () => ({
|
|
Agent: class {
|
|
state = { errorMessage: undefined, messages: [] };
|
|
private readonly tools: typeof fakeAgentState.tools;
|
|
|
|
constructor(options: { initialState: { tools: typeof fakeAgentState.tools } }) {
|
|
this.tools = options.initialState.tools;
|
|
fakeAgentState.tools = this.tools;
|
|
}
|
|
|
|
subscribe(_listener: unknown) {}
|
|
|
|
async prompt() {
|
|
const askUser = this.tools.find((tool) => tool.name === "ask_user");
|
|
if (!askUser) throw new Error("ask_user tool missing");
|
|
await askUser.execute("call-choice-1", {
|
|
question: "Which city should I use?",
|
|
options: fakeAgentState.options,
|
|
});
|
|
}
|
|
|
|
async waitForIdle() {}
|
|
|
|
abort() {}
|
|
},
|
|
}));
|
|
|
|
vi.mock("@earendil-works/pi-ai/providers/all", () => ({
|
|
builtinModels: () => ({
|
|
getModel: (_provider: string, modelId: string) =>
|
|
modelId === "choice-pi-model" ? { provider: "test", id: modelId } : undefined,
|
|
streamSimple: () => {
|
|
throw new Error("the fake agent must not call a provider");
|
|
},
|
|
}),
|
|
}));
|
|
|
|
vi.mock("./pi-local-provider.js", () => ({
|
|
registerLocalProvider: (models: unknown) => models,
|
|
}));
|
|
|
|
vi.mock("./pi-openai-compatible-provider.js", () => ({
|
|
OPENAI_COMPATIBLE_PROVIDER_ID: "openai-compatible",
|
|
registerOpenAiCompatibleCatalog: (models: unknown) => models,
|
|
registerOpenAiCompatibleRuntime: (models: unknown) => models,
|
|
}));
|
|
|
|
import { PiAgentRuntime } from "./pi-runtime.js";
|
|
|
|
const choiceTool: ConnectorTool = {
|
|
name: "ask_user",
|
|
description: "Ask a short multiple-choice question",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
question: { type: "string" },
|
|
options: { type: "array", items: { type: "string" } },
|
|
},
|
|
required: ["question", "options"],
|
|
},
|
|
};
|
|
|
|
const runContext = {
|
|
operationId: "choice-pi",
|
|
traceId: "choice-pi",
|
|
spaceId: "space",
|
|
userId: "user",
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
const runRequest = {
|
|
botId: "bot",
|
|
threadId: "thread",
|
|
runId: "run",
|
|
prompt: "ask me which city to use",
|
|
instructions: "Use ask_user for a short list of options.",
|
|
history: [],
|
|
tools: [choiceTool],
|
|
model: { provider: "test", id: "choice-pi-model" },
|
|
};
|
|
|
|
describe("Pi choice asks", () => {
|
|
beforeEach(() => {
|
|
fakeAgentState.tools = [];
|
|
fakeAgentState.options = ["Berlin", "Seoul", "Toronto"];
|
|
});
|
|
|
|
it("emits a tappable ask and stops without a finished-work fallback", async () => {
|
|
const runtime = new PiAgentRuntime();
|
|
const events: AgentRuntimeEvent[] = [];
|
|
|
|
for await (const event of runtime.run(runRequest, runContext)) events.push(event);
|
|
|
|
expect(events).toContainEqual({
|
|
type: "ask",
|
|
text: "Which city should I use?",
|
|
actions: [
|
|
{ id: "choice-1", label: "Berlin" },
|
|
{ id: "choice-2", label: "Seoul" },
|
|
{ id: "choice-3", label: "Toronto" },
|
|
],
|
|
});
|
|
expect(
|
|
events.some((event) => event.type === "text" && event.text.includes("I finished the work.")),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects an empty choice set before emitting an ask", async () => {
|
|
fakeAgentState.options = [];
|
|
const runtime = new PiAgentRuntime();
|
|
const events: AgentRuntimeEvent[] = [];
|
|
|
|
await expect(async () => {
|
|
for await (const event of runtime.run(runRequest, runContext)) events.push(event);
|
|
}).rejects.toThrow("ask_user requires two to four unique, non-empty options");
|
|
expect(events.some((event) => event.type === "ask")).toBe(false);
|
|
});
|
|
});
|