Files
2026-07-13 13:39:12 +08:00

324 lines
10 KiB
JavaScript

import { readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch, isServerUp } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const A2A_SKILLS = [
{ id: "smart-routing", name: "Smart Request Routing" },
{ id: "quota-management", name: "Quota & Cost Management" },
{ id: "provider-discovery", name: "Provider Discovery" },
{ id: "cost-analysis", name: "Cost Analysis" },
{ id: "health-report", name: "Health Report" },
];
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
function randomId() {
return Math.random().toString(36).slice(2, 11);
}
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
const taskSchema = [
{ key: "id", header: "Task ID", width: 22 },
{ key: "skill", header: "Skill", width: 22 },
{ key: "status", header: "Status", width: 12 },
{ key: "createdAt", header: "Created", formatter: fmtTs },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
{ key: "duration", header: "Duration", formatter: (v) => (v != null ? `${v}ms` : "-") },
];
export function registerA2a(program) {
const a2a = program.command("a2a").description("Agent-to-Agent (A2A) server");
a2a
.command("status")
.description("Show A2A server status")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runA2aStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
a2a
.command("card")
.description("Print the Agent Card JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runA2aCardCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// 5.3 — a2a skills + a2a invoke
a2a
.command("skills")
.description(t("a2a.skills.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/.well-known/agent.json");
if (res.ok) {
const card = await res.json();
emit(card.skills ?? A2A_SKILLS, cmd.optsWithGlobals());
} else {
emit(A2A_SKILLS, cmd.optsWithGlobals());
}
});
a2a
.command("invoke <skill>")
.description(t("a2a.invoke.description"))
.option("--input <json>", t("a2a.invoke.input"))
.option("--input-file <path>", t("a2a.invoke.input_file"))
.option("--wait", t("a2a.invoke.wait"))
.option("--timeout <ms>", t("a2a.invoke.timeout"), parseInt, 60000)
.action(async (skill, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const input = opts.input
? JSON.parse(opts.input)
: opts.inputFile
? JSON.parse(readFileSync(opts.inputFile, "utf8"))
: {};
const rpcBody = {
jsonrpc: "2.0",
id: randomId(),
method: "tasks.create",
params: {
skill,
input,
messages: [{ role: "user", parts: [{ kind: "data", data: input }] }],
},
};
const res = await apiFetch("/api/a2a/tasks", { method: "POST", body: rpcBody });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const created = await res.json();
const taskId = created.result?.taskId ?? created.taskId ?? created.id;
if (!opts.wait) {
emit({ taskId }, globalOpts);
return;
}
const deadline = Date.now() + (opts.timeout ?? 60000);
while (Date.now() < deadline) {
await sleep(1000);
const taskRes = await apiFetch(`/api/a2a/tasks/${taskId}`);
if (!taskRes.ok) continue;
const task = (await taskRes.json()).result ?? (await taskRes.clone().json());
const state = task.status?.state ?? task.status;
if (["completed", "failed", "cancelled"].includes(state)) {
emit(task, globalOpts);
return;
}
}
process.stderr.write("Timeout waiting for task completion\n");
process.exit(124);
});
// 5.4 — a2a tasks
const tasks = a2a.command("tasks").description(t("a2a.tasks.description"));
tasks
.command("list")
.option("--status <s>", t("a2a.tasks.list.status"))
.option("--skill <s>", t("a2a.tasks.list.skill"))
.option("--limit <n>", parseInt, 50)
.option("--since <ts>")
.action(async (opts, cmd) => {
const params = new URLSearchParams({ limit: String(opts.limit ?? 50) });
if (opts.status) params.set("status", opts.status);
if (opts.skill) params.set("skill", opts.skill);
if (opts.since) params.set("since", opts.since);
const res = await apiFetch(`/api/a2a/tasks?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.tasks ?? data.items ?? data, cmd.optsWithGlobals(), taskSchema);
});
tasks.command("get <id>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/a2a/tasks/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tasks
.command("cancel <id>")
.option("--yes")
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Cancel task ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/a2a/tasks/${id}/cancel`, { method: "POST" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Cancelled\n");
});
tasks
.command("watch <id>")
.description(t("a2a.tasks.watch.description"))
.action(async (id, opts, cmd) => {
let lastState = "";
while (true) {
const res = await apiFetch(`/api/a2a/tasks/${id}`);
if (res.ok) {
const data = await res.json();
const state = data.status?.state ?? data.status ?? "";
if (state !== lastState) {
process.stderr.write(`[${new Date().toISOString()}] ${state}\n`);
lastState = state;
}
if (["completed", "failed", "cancelled"].includes(state)) {
emit(data, cmd.optsWithGlobals());
return;
}
}
await sleep(1500);
}
});
tasks
.command("stream <id>")
.description(t("a2a.tasks.stream.description"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
const apiKey = globalOpts.apiKey ?? "";
const res = await fetch(`${baseUrl}/api/a2a/tasks/${id}?stream=true`, {
headers: {
Accept: "text/event-stream",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
}
});
tasks
.command("logs <id>")
.description(t("a2a.tasks.logs.description"))
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/a2a/tasks/${id}?include=messages,artifacts`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.messages ?? data, cmd.optsWithGlobals());
});
}
export async function runA2aStatusCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/a2a/status", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.log("A2A status not available.");
return 0;
}
const status = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(status, null, 2));
return 0;
}
const running = status.running ? "\x1b[32mrunning\x1b[0m" : "\x1b[31mstopped\x1b[0m";
console.log(` Status: ${running}`);
console.log(` Protocol: ${status.protocol || "JSON-RPC 2.0"}`);
console.log(` Tasks: ${status.activeTasks || 0} active`);
if (status.skills?.length) {
console.log("\n Skills:");
for (const skill of status.skills) {
console.log(`\x1b[2m - ${skill.name}: ${skill.description || "N/A"}\x1b[0m`);
}
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runA2aCardCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/.well-known/agent.json", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
const card = await res.json();
console.log(JSON.stringify(card, null, 2));
return 0;
}
console.log("Agent card not available.");
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}