docs(spikes): Deno 2.9.7 + TS 7 compatibility spike and ADR-004
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
fe3613ee8f
commit
cf85481d95
147 files changed
+15213
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import process from "node:process";
|
||||
// Modes: abort | crash | resume. Uses a file-backed JSONL session so a second process can recover.
|
||||
import { createModels, fauxProvider } from "@earendil-works/pi-ai";
|
||||
import { AgentHarness, BACKGROUND_CONTEXT, JsonlSessionRepo } from "@earendil-works/pi-agent-core";
|
||||
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/harness/env/nodejs";
|
||||
import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts";
|
||||
|
||||
const mode = process.argv[2];
|
||||
const ctx = BACKGROUND_CONTEXT;
|
||||
const faux = fauxProvider({ provider: "umt-fake" });
|
||||
const models = createModels();
|
||||
models.setProvider(faux.provider);
|
||||
faux.setResponses(mode === "resume" ? [script[2]!] : script);
|
||||
|
||||
const root = new URL("./.sessions", import.meta.url).pathname;
|
||||
const fs = new NodeExecutionEnv({ cwd: root }); // used only as FileSystem for session storage; no shell tool is exposed
|
||||
const repo = new JsonlSessionRepo({ fileSystem: fs, sessionsRoot: root });
|
||||
let session;
|
||||
if (mode === "resume") {
|
||||
const list = await repo.list({ cwd: root }, ctx);
|
||||
const meta = list.find((m) => m.id === "crash-demo");
|
||||
console.log("found session", meta?.id, meta?.path);
|
||||
session = await repo.open(meta!, ctx);
|
||||
} else {
|
||||
session = await repo.create({ id: mode === "crash" ? "crash-demo" : `abort-${Date.now()}`, cwd: root }, ctx);
|
||||
}
|
||||
|
||||
const { harness, open } = await AgentHarness.create({
|
||||
session, models, model: faux.getModel(), systemPrompt: "媒体库助手",
|
||||
tools: [inventoryQuery, downloadSubmit], toolContext: { actorId: "user:1", grants: ["download.submit"] },
|
||||
toolExecution: "sequential",
|
||||
}, ctx);
|
||||
console.log("open operations at attach:", JSON.stringify(open));
|
||||
|
||||
let lane = await harness.lane("main", ctx);
|
||||
for (const t of ["tool_start", "tool_end", "run_end", "operation_abort", "run_resume"] as const)
|
||||
harness.events.on(t as any, async (e: any) => {
|
||||
console.log(`[${e.type}]${e.recovery ? " (recovery)" : ""} ${e.toolName ?? ""} ${e.status ?? ""} ${e.result ? JSON.stringify(e.result.content[0]?.text).slice(0, 220) : ""}`);
|
||||
if (e.type === "tool_start" && e.toolName === "download__submit") {
|
||||
if (mode === "abort") setTimeout(async () => { console.log(">> lane.abort()"); console.log("abort result:", JSON.stringify(await lane.abort(ctx))); }, 300);
|
||||
if (mode === "crash") setTimeout(() => { console.log(">> SIGKILL self during download.submit"); process.kill(process.pid, "SIGKILL"); }, 300);
|
||||
}
|
||||
});
|
||||
|
||||
if (mode === "resume") {
|
||||
const r = await lane.resume(ctx);
|
||||
console.log("RESUME RESULT", JSON.stringify(r));
|
||||
} else {
|
||||
const r = await lane.prompt("把《Dune》1080p 收进来", undefined, ctx);
|
||||
console.log("PROMPT RESULT", JSON.stringify(r));
|
||||
}
|
||||
console.log("SUBMITTED in this process", JSON.stringify(submitted), "fake calls:", faux.state.callCount);
|
||||
const entries = (await lane.findEntries(undefined, ctx)).reverse();
|
||||
for (const e of entries as any[]) {
|
||||
if (e.type !== "message") { console.log(" entry", e.type); continue; }
|
||||
const m = e.message;
|
||||
const text = m.role === "assistant" ? m.content.map((c: any) => c.type === "toolCall" ? `toolCall(${c.name})` : c.type === "text" ? c.text : `<${c.type}>`).join(" | ")
|
||||
: Array.isArray(m.content) ? m.content.map((c: any) => c.text).join("") : m.content;
|
||||
console.log(` ${m.role.padEnd(10)} ${m.toolName ?? ""} ${m.isError ? "[isError]" : ""} ${String(text).slice(0, 200)}`);
|
||||
}
|
||||
await harness.close(ctx);
|
||||
@@ -0,0 +1,84 @@
|
||||
import process from "node:process";
|
||||
// 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: any, 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: any, _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}。`,
|
||||
);
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,51 @@
|
||||
import process from "node:process";
|
||||
// Fake model drives AgentHarness through 2 whitelisted tools; streams events.
|
||||
import { createModels, fauxProvider } from "@earendil-works/pi-ai";
|
||||
import { AgentHarness, BACKGROUND_CONTEXT, MemorySessionRepo } from "@earendil-works/pi-agent-core";
|
||||
import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts";
|
||||
|
||||
const faux = fauxProvider({ provider: "umt-fake" });
|
||||
const models = createModels();
|
||||
models.setProvider(faux.provider);
|
||||
faux.setResponses(script);
|
||||
|
||||
const repo = new MemorySessionRepo();
|
||||
const session = await repo.create({ id: "spike-1" }, BACKGROUND_CONTEXT);
|
||||
const { harness } = await AgentHarness.create({
|
||||
session, models, model: faux.getModel(),
|
||||
systemPrompt: "你是媒体库自动化助手,只能使用提供的工具。",
|
||||
tools: [inventoryQuery, downloadSubmit], // explicit whitelist; no discovery
|
||||
toolContext: { actorId: "user:1", grants: ["download.submit"] },
|
||||
thinkingLevel: "off",
|
||||
compaction: { enabled: true, reserveTokens: 4096, keepRecentTokens: 8000 },
|
||||
retry: { enabled: true, maxRetries: 2, baseDelayMs: 100 },
|
||||
toolExecution: "sequential",
|
||||
}, BACKGROUND_CONTEXT);
|
||||
|
||||
// Server-side authorization gate in addition to the whitelist.
|
||||
harness.hooks.on("before_tool", (ev) => {
|
||||
console.log(` [hook before_tool] ${ev.toolName} args=${JSON.stringify(ev.args)}`);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const types = ["run_start", "turn_start", "message_update", "tool_start", "tool_update", "tool_end", "turn_end", "run_end", "usage", "fault", "handler_error"] as const;
|
||||
for (const t of types) harness.events.on(t as any, (e: any) => {
|
||||
if (e.type === "message_update") {
|
||||
if (e.event.type === "text_delta") process.stdout.write(` [text_delta] ${JSON.stringify(e.event.delta)}\n`);
|
||||
else if (e.event.type === "toolcall_end") console.log(` [toolcall_end] ${e.event.toolCall.name}`);
|
||||
return;
|
||||
}
|
||||
if (e.type === "tool_end") return console.log(`[tool_end] ${e.toolName} isError=${e.isError} -> ${JSON.stringify(e.result.content[0]?.text)}`);
|
||||
if (e.type === "tool_start") return console.log(`[tool_start] ${e.toolName} ${JSON.stringify(e.args)}`);
|
||||
if (e.type === "usage") return console.log(`[usage] in=${e.row?.usage?.input ?? "?"} totals.input=${e.totals.input} output=${e.totals.output}`);
|
||||
if (e.type === "run_end") return console.log(`[run_end] status=${e.status}${e.error ? " error=" + JSON.stringify(e.error) : ""}`);
|
||||
console.log(`[${e.type}]${e.lane ? " lane=" + e.lane : ""}`);
|
||||
});
|
||||
|
||||
const lane = await harness.lane("main", BACKGROUND_CONTEXT);
|
||||
const result = await lane.prompt("把《Dune》1080p 收进来", undefined, BACKGROUND_CONTEXT);
|
||||
console.log("RESULT", JSON.stringify(result));
|
||||
console.log("SUBMITTED", JSON.stringify(submitted), "fake calls:", faux.state.callCount);
|
||||
const entries = await lane.findEntries(undefined, BACKGROUND_CONTEXT);
|
||||
console.log("TRANSCRIPT roles:", entries.map((e: any) => e.type === "message" ? e.message.role : e.type).join(","));
|
||||
await harness.close(BACKGROUND_CONTEXT);
|
||||
Reference in new issue
Block a user