Files
agent/scripts/verify-live-model.mjs
T

90 lines
4.9 KiB
JavaScript

import { createInterface } from "node:readline";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const base = "http://127.0.0.1:3001";
const input = createInterface({ input: process.stdin });
console.log("Awaiting in-memory credential (no file persistence)");
const key = (await input[Symbol.asyncIterator]().next()).value?.trim();
input.close();
if (!key) throw new Error("Credential required on stdin");
const api = process.argv[2] ?? "openai-responses";
const existing = process.argv[3];
const safe = (value) => JSON.stringify(value).replaceAll(key, "[REDACTED]");
async function request(path, body, method = "POST") {
const response = await fetch(base + path, {
method,
...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }),
signal: AbortSignal.timeout(30_000),
});
const result = await response.json();
if (!response.ok) throw new Error(safe({ status: response.status, result }));
return result;
}
const { sessionId } = existing ? { sessionId: existing } : await request("/api/sessions");
console.log(safe({ sessionId, api }));
await request(`/api/sessions/${sessionId}/model-config`, {
providerId: "compatible", api, baseUrl: "https://gateway.pencil-stub.com/v1",
apiKey: key, modelId: "Qwen3.8-27B-FP8", supportsImages: false, maxTokens: 4096,
}, "PUT");
console.log("Model configured in Session memory");
async function run(text, approve = false) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 150_000);
const response = await fetch(`${base}/api/sessions/${sessionId}/events`, { signal: controller.signal });
const reader = response.body.getReader();
const sent = await request(`/api/sessions/${sessionId}/messages`, { text, attachmentIds: [] });
const events = [];
let pending = "";
let answer = "";
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
pending += decoder.decode(value, { stream: true });
let boundary;
while ((boundary = pending.indexOf("\n\n")) !== -1) {
const frame = pending.slice(0, boundary);
pending = pending.slice(boundary + 2);
const data = frame.split("\n").find((line) => line.startsWith("data: "))?.slice(6);
if (!data) continue;
const event = JSON.parse(data);
if (event.runId !== sent.runId) continue;
events.push(event);
if (event.type === "assistant.delta") answer += event.payload.delta;
else console.log(safe({ type: event.type, payload: event.type === "tool.completed" && event.payload.toolName === "read" ? { toolName: "read", toolCallId: event.payload.toolCallId } : event.payload }));
if (event.type === "approval.required") {
if (!approve || event.payload.toolName !== "save_report_draft") throw new Error("Unexpected approval request");
if (process.env.LIVE_APPROVAL_MODE !== "manual") await request(`/api/approvals/${event.payload.approvalId}/approve`);
}
if (["run.completed", "run.failed", "run.cancelled"].includes(event.type)) {
console.log(safe({ runId: sent.runId, answer, deltaCount: events.filter((e) => e.type === "assistant.delta").length }));
if (event.type !== "run.completed" || !answer.trim()) throw new Error("Run did not produce a successful visible answer");
return events;
}
}
}
throw new Error("Event stream ended without a terminal run event");
} finally {
clearTimeout(timer);
controller.abort();
}
}
try {
await run("请只回复:连接成功。不要调用工具。");
const events = await run("请分析 demo-account 最近 7 天广告数据,并调用 save_report_draft 保存中文报告。先读取 ads-analysis 和 report-writer 两个 Skill,再调用 mock_ads_metrics。报告正文严格控制在 400 字内,保留五个章节,每章 1 至 2 句,不要表格;数字引用工具结果,注明模拟数据。写入由本次测试批准,成功后只回复保存成功及路径。", true);
for (const name of ["mock_ads_metrics", "save_report_draft"]) {
if (!events.some((e) => e.type === "tool.completed" && e.payload.toolName === name)) throw new Error(`Tool did not complete: ${name}`);
}
if (!events.some((e) => e.type === "approval.resolved" && e.payload.status === "approved")) throw new Error("Approval was not observed");
if (events.filter((e) => e.type === "skill.loaded").length < 2) throw new Error("Both Skills must be loaded successfully");
const report = await readFile(fileURLToPath(new URL(`../.agent-studio-sessions/${sessionId}/report.md`, import.meta.url)), "utf8");
console.log(safe({ result: "PASS", sessionId, reportCharacters: report.length, skillLoaded: events.filter((e) => e.type === "skill.loaded").length }));
} catch (error) {
console.error(safe({ result: "FAIL", message: error.message, sessionId }));
process.exitCode = 1;
}