84 lines
4.2 KiB
TypeScript
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}。`,
|
|
);
|
|
},
|
|
];
|