Files
UHD_Media_Tool/spikes/pi/common.ts
T

84 lines
4.2 KiB
TypeScript

// Shared pieces for the Pi spike: whitelisted business tools + scripted fake model.
import { Type } from "typebox";
import {
fauxAssistantMessage, fauxText, fauxThinking, fauxToolCall,
type FauxResponseFactory,
} from "@earendil-works/pi-ai";
import type { AgentHarnessTool } from "@earendil-works/pi-agent-core";
// Capability id -> provider-safe tool name. Anthropic/OpenAI tool names must match ^[a-zA-Z0-9_-]{1,64}$,
// so "inventory.query" cannot be sent verbatim; the port maps ids <-> names.
export const toolName = (capabilityId: string) => capabilityId.replace(/\./g, "__");
export const submitted: Array<{ key: string; title: string }> = [];
type Ctx = { actorId: string; grants: string[] };
export const inventoryQuery: AgentHarnessTool<Ctx> = {
name: toolName("inventory.query"),
label: "inventory.query",
description: "Query local library inventory for a title. Returns whether it exists and which versions.",
parameters: Type.Object({ title: Type.String(), year: Type.Optional(Type.Number()) }),
replay: "safe", // read-only: harness may re-execute after a crash
async execute(_id, params, onUpdate, toolCtx, invocation, context) {
onUpdate({ content: [{ type: "text", text: "searching…" }], details: {} });
const found = params.title === "Arrival";
return {
content: [{ type: "text", text: JSON.stringify({ title: params.title, inLibrary: found, versions: found ? ["2160p"] : [] }) }],
details: { actor: toolCtx.actorId, invocationId: invocation.invocationId, aborted: context.abortSignal?.aborted ?? false },
};
},
};
export const downloadSubmit: AgentHarnessTool<Ctx> = {
name: toolName("download.submit"),
label: "download.submit",
description: "Submit a download plan. Server validates authorization and generates the idempotency key.",
parameters: Type.Object({ title: Type.String(), quality: Type.String() }),
replay: "never", // external side effect: after crash the harness synthesizes an 'interrupted' result instead of re-running
executionMode: "sequential",
async execute(_id, params, _onUpdate, toolCtx, invocation, context) {
if (!toolCtx.grants.includes("download.submit")) throw new Error("not authorized: download.submit");
// Server-side idempotency key derived from operation semantics, not from the model.
const key = `dl:${params.title}:${params.quality}`;
const delayMs = Number(process.env.SUBMIT_DELAY_MS ?? 0);
if (delayMs) {
await new Promise<void>((resolve, reject) => {
const t = setTimeout(resolve, delayMs);
context.abortSignal?.addEventListener("abort", () => { clearTimeout(t); reject(new Error("aborted by signal")); });
});
}
if (!submitted.find((s) => s.key === key)) submitted.push({ key, title: params.title });
await invocation.setMemo("submitted", key);
return { content: [{ type: "text", text: JSON.stringify({ accepted: true, idempotencyKey: key, status: "submitted" }) }], details: { key } };
},
};
const lastToolResult = (ctx: any, name: string) =>
[...ctx.messages].reverse().find((m: any) => m.role === "toolResult" && m.toolName === name);
// Scripted fake model: each step is a factory that looks at the transcript (tool results) and decides.
export const script: FauxResponseFactory[] = [
() => fauxAssistantMessage([
fauxThinking("先查本地库存"),
fauxText("我先查一下库里有没有《Dune》。"),
fauxToolCall(toolName("inventory.query"), { title: "Dune", year: 2021 }),
], { stopReason: "toolUse" }),
(ctx) => {
const inv = JSON.parse(lastToolResult(ctx, toolName("inventory.query")).content[0].text);
if (inv.inLibrary) return fauxAssistantMessage("库里已有,无需下载。");
return fauxAssistantMessage([
fauxText("库里没有,提交 1080p 下载;顺便试一下未授权的 bash。"),
fauxToolCall(toolName("download.submit"), { title: "Dune", quality: "1080p" }),
fauxToolCall("bash", { command: "rm -rf /" }),
], { stopReason: "toolUse" });
},
(ctx) => {
const dl = lastToolResult(ctx, toolName("download.submit"));
const bash = lastToolResult(ctx, "bash");
return fauxAssistantMessage(
`下载已提交(仅代表已提交,不代表已入库):${dl.content[0].text};bash 结果 isError=${bash?.isError}。`,
);
},
];