Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/demo/provider.ts

Raw
/**
 * Demo provider: a Pi extension loaded only while recording README demos.
 * Registers the `demo` provider with scripted assistant turns read from the
 * scene file named by `PI_DEMO_SCENE`, so recordings are deterministic and
 * cost nothing. The scene's `turns` array supplies one reply per user turn;
 * when it runs out, the last reply repeats.
 */
import { readFileSync, writeFileSync } from "node:fs";
import {
	createFauxCore,
	type FauxResponseStep,
	fauxAssistantMessage,
	fauxToolCall,
	type ToolCall,
} from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export const DEMO_PROVIDER = "demo";
export const DEMO_MODEL = "demo-model";
/** A second, pricier model so model-switching demos have somewhere to go. */
export const DEMO_DEEP_MODEL = "demo-deep";

interface SceneTurn {
	text?: string;
	tool?: { name: string; arguments: ToolCall["arguments"] };
	/** Token usage reported for this turn so cost and context segments move. */
	usage?: { input?: number; output?: number; cacheRead?: number };
}

interface SceneFile {
	turns?: SceneTurn[];
	/** Replies for every child session an extension spawns in-process (for
	 * example ultra sub-agents); each child replays this list from the start,
	 * so concurrent children stay deterministic. */
	childTurns?: SceneTurn[];
}

interface Report {
	queued: number;
	calls: number;
	instances: number;
	childCalls: number;
}

/** `{slot.id}` and `{slot.path}` in a turn resolve to the most recent
 * `system_bundle_slot` result in the transcript, so a scripted write can
 * target an ID the-system generated at runtime. */
function substituteSlot(value: unknown, transcript: string): unknown {
	if (typeof value === "string") {
		if (
			!value.includes("{slot.") &&
			!value.includes("{spec.") &&
			!value.includes("{prompt:")
		)
			return value;
		// `{prompt:REGEX}`: first capture group of REGEX over the latest user
		// message, for example a fanout item path inside an ultra envelope.
		const withPrompt = value.replaceAll(
			/\{prompt:((?:[^{}\\]|\\.)+)\}/gu,
			(_m, pattern: string) =>
				new RegExp(pattern, "u").exec(
					transcript.split("\u0000")[1] ?? "",
				)?.[1] ?? "",
		);
		const id = /ID: (\S+)/u.exec(transcript)?.[1] ?? "";
		const path = /Path: (\S+)/u.exec(transcript)?.[1] ?? "";
		// `{spec.id}`: the most recent reserved spec, for plans that reference it.
		const spec =
			/Reserved spec bundle\.\s*Title: [^\n]*\s*ID: (\S+)/u.exec(
				transcript,
			)?.[1] ?? "";
		return withPrompt
			.replaceAll("{slot.id}", id)
			.replaceAll("{slot.path}", path)
			.replaceAll("{spec.id}", spec);
	}
	if (Array.isArray(value))
		return value.map((v) => substituteSlot(v, transcript));
	if (value && typeof value === "object")
		return Object.fromEntries(
			Object.entries(value).map(([k, v]) => [k, substituteSlot(v, transcript)]),
		);
	return value;
}

/** Transcript text of every bundle reservation so far, newest first, so
 * `{slot.*}` resolves to the latest and `{spec.id}` to the latest spec. */
function lastSlotText(context: {
	messages: Array<{ role: string; content: unknown }>;
}): string {
	const parts: string[] = [];
	let lastUser = "";
	for (let i = context.messages.length - 1; i >= 0; i--) {
		const message = context.messages[i];
		if (message?.role === "user" && !lastUser)
			lastUser = JSON.stringify(message.content);
		if (message?.role !== "toolResult") continue;
		const text = JSON.stringify(message.content).replaceAll("\\n", "\n");
		if (text.includes("Reserved ") && text.includes("ID: ")) parts.push(text);
	}
	// Slot texts, then a NUL, then the latest user message for `{prompt:…}`.
	return `${parts.join("\n")}\u0000${lastUser}`;
}

function readScene(): SceneFile {
	const path = process.env["PI_DEMO_SCENE"];
	if (!path) return { turns: [{ text: "Hello from the demo model." }] };
	return JSON.parse(readFileSync(path, "utf8")) as SceneFile;
}

export default function demoProvider(pi: ExtensionAPI): void {
	const scene = readScene();
	const turns = scene.turns ?? [];
	const core = createFauxCore({
		api: "openai",
		provider: DEMO_PROVIDER,
		models: [
			{ id: DEMO_MODEL, name: "Demo Model" },
			{ id: DEMO_DEEP_MODEL, name: "Demo Deep" },
		],
	});
	// The runner compares calls made with turns queued after the scene ends.
	// Pi instantiates this extension again for /new, ctx.newSession, and
	// every in-process child session, so instances coordinate through the
	// report file: the primary queue continues where the previous primary
	// stopped, and child sessions (when the scene declares childTurns) each
	// replay childTurns from the start.
	const reportPath = process.env["PI_DEMO_REPORT"];
	let previous: Report = { queued: 0, calls: 0, instances: 0, childCalls: 0 };
	if (reportPath) {
		try {
			previous = JSON.parse(readFileSync(reportPath, "utf8")) as Report;
		} catch {
			// first instance
		}
	}
	const isChild = Boolean(scene.childTurns) && previous.instances > 0;
	const queue = isChild
		? (scene.childTurns ?? [])
		: turns.slice(previous.calls);
	if (reportPath) {
		const write = () =>
			writeFileSync(
				reportPath,
				JSON.stringify({
					queued: turns.length,
					instances: previous.instances + 1,
					calls: isChild
						? previous.calls
						: previous.calls + core.state.callCount,
					childCalls: isChild
						? previous.childCalls + core.state.callCount
						: previous.childCalls,
				} satisfies Report),
			);
		write();
		pi.on("turn_end", write);
		pi.on("session_shutdown", write);
	}
	const build = (turn: SceneTurn, transcript: string) => {
		const text = substituteSlot(turn.text, transcript) as string | undefined;
		const message = turn.tool
			? fauxAssistantMessage([
					...(text ? [{ type: "text" as const, text }] : []),
					fauxToolCall(
						turn.tool.name,
						substituteSlot(
							turn.tool.arguments,
							transcript,
						) as ToolCall["arguments"],
					),
				])
			: fauxAssistantMessage(text ?? "");
		if (turn.usage) {
			const input = turn.usage.input ?? 0;
			const output = turn.usage.output ?? 0;
			const cacheRead = turn.usage.cacheRead ?? 0;
			message.usage = {
				...message.usage,
				input,
				output,
				cacheRead,
				cacheWrite: 0,
				totalTokens: input + output + cacheRead,
			};
		}
		return message;
	};
	// PI_DEMO_TRACE=<file>: append one line per model call with the system
	// prompt head and the latest user text, to align scene.turns with what Pi
	// actually asks (compaction and sub-agent calls are invisible on screen).
	const tracePath = process.env["PI_DEMO_TRACE"];
	const trace = (
		index: number,
		context: {
			systemPrompt?: string;
			messages: Array<{ role: string; content: unknown }>;
		},
	) => {
		if (!tracePath) return;
		const lastUser = [...context.messages]
			.reverse()
			.find((m) => m.role === "user");
		const head = (text: unknown) => JSON.stringify(text ?? "").slice(0, 160);
		writeFileSync(
			tracePath,
			`${isChild ? "child" : "main"} #${index + 1} system=${head(context.systemPrompt)} user=${head(lastUser?.content)}\n`,
			{ flag: "a" },
		);
	};
	core.setResponses(
		queue.map(
			(turn, index): FauxResponseStep =>
				(context) => {
					const typed = context as unknown as {
						systemPrompt?: string;
						messages: Array<{ role: string; content: unknown }>;
					};
					trace(index, typed);
					return build(turn, lastSlotText(typed));
				},
		),
	);
	pi.registerProvider(DEMO_PROVIDER, {
		apiKey: "demo",
		api: "openai",
		baseUrl: "https://demo.invalid",
		streamSimple: core.streamSimple,
		models: [
			{
				id: DEMO_MODEL,
				name: "Demo Model",
				reasoning: false,
				input: ["text"],
				cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
				contextWindow: 200_000,
				maxTokens: 8192,
			},
			{
				id: DEMO_DEEP_MODEL,
				name: "Demo Deep",
				reasoning: true,
				input: ["text"],
				cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
				contextWindow: 1_000_000,
				maxTokens: 32_000,
			},
		],
	});
}