openclaw-multi-session-plugins/dist/src/bridgeAgents.js

206 lines
7.6 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
import { exportXWorkmateArtifacts, prepareXWorkmateArtifacts, } from "./exportArtifacts.js";
export async function runXWorkmateBridgeAgents(input) {
const params = input.params ?? {};
const pluginConfig = input.pluginConfig ?? {};
const sessionKey = requiredString(params.sessionKey, "sessionKey required");
const runId = requiredString(params.runId, "runId required");
const taskPrompt = requiredString(params.taskPrompt, "taskPrompt required");
const bridgeUrl = bridgeRpcUrl(pluginConfig);
const bridgeToken = bridgeAuthToken(pluginConfig);
if (!bridgeToken) {
throw new Error("bridgeToken required");
}
const prepared = await prepareXWorkmateArtifacts({
params: { sessionKey, runId, workspaceDir: params.workspaceDir },
config: input.config,
pluginConfig,
});
const orchestrationMode = optionalString(params.mode) || optionalString(params.orchestrationMode) || "sequence";
const participants = safeStringList(params.participants);
const steps = safeSteps(params.steps, participants.length > 0);
if (steps.length === 0 && participants.length === 0) {
throw new Error("steps or participants required");
}
const routing = {
orchestrationMode,
steps,
};
if (participants.length > 0) {
routing.participants = participants;
}
const maxTurns = positiveInteger(params.maxTurns, 0);
if (maxTurns > 0) {
routing.maxTurns = maxTurns;
}
const stopConditions = safeStringList(params.stopConditions);
if (stopConditions.length > 0) {
routing.stopConditions = stopConditions;
}
const bridgeResult = await callBridgeRPC({
bridgeUrl,
bridgeToken,
timeoutMs: positiveInteger(params.timeoutMs, positiveInteger(pluginConfig.bridgeTimeoutMs, 600_000)),
body: {
jsonrpc: "2.0",
id: `openclaw-${Date.now()}`,
method: "session.start",
params: {
sessionId: `openclaw:${sessionKey}`,
threadId: sessionKey,
taskPrompt,
workingDirectory: prepared.artifactDirectory,
multiAgent: true,
mode: "multi-agent",
routing,
},
},
});
await fs.mkdir(prepared.artifactDirectory, { recursive: true });
await fs.writeFile(path.join(prepared.artifactDirectory, "multi-agent-result.json"), `${JSON.stringify(bridgeResult, null, 2)}\n`);
await fs.writeFile(path.join(prepared.artifactDirectory, "multi-agent-result.md"), formatBridgeResultMarkdown(bridgeResult));
const exported = await exportXWorkmateArtifacts({
params: {
sessionKey,
runId,
workspaceDir: params.workspaceDir,
artifactScope: prepared.artifactScope,
includeContent: false,
},
config: input.config,
pluginConfig,
});
return { ...exported, bridgeResult };
}
async function callBridgeRPC(input) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), input.timeoutMs);
try {
const response = await fetch(input.bridgeUrl, {
method: "POST",
headers: {
Authorization: bearer(input.bridgeToken),
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(input.body),
signal: controller.signal,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`bridge request failed (${response.status}): ${text.trim()}`);
}
const decoded = JSON.parse(text);
const error = asRecord(decoded.error);
if (error) {
throw new Error(optionalString(error.message) || "bridge rpc error");
}
const result = asRecord(decoded.result);
if (!result) {
throw new Error("bridge response missing result");
}
return result;
}
finally {
clearTimeout(timer);
}
}
function bridgeRpcUrl(pluginConfig) {
const configured = optionalString(pluginConfig.bridgeUrl) || optionalString(process.env.XWORKMATE_BRIDGE_URL);
if (!configured) {
throw new Error("bridgeUrl required");
}
const trimmed = configured.replace(/\/+$/, "");
if (trimmed.endsWith("/acp/rpc")) {
return trimmed;
}
return `${trimmed}/acp/rpc`;
}
function bridgeAuthToken(pluginConfig) {
return optionalString(pluginConfig.bridgeToken) || optionalString(process.env.XWORKMATE_BRIDGE_TOKEN);
}
function safeSteps(raw, allowEmpty) {
if (!Array.isArray(raw)) {
if (allowEmpty) {
return [];
}
throw new Error("steps required");
}
return raw.map((item, index) => {
const mapped = asRecord(item);
if (!mapped) {
throw new Error(`steps[${index}] must be an object`);
}
const providerId = optionalString(mapped.providerId) || optionalString(mapped.provider) || optionalString(mapped.agent);
const prompt = optionalString(mapped.prompt) || optionalString(mapped.taskPrompt);
if (!providerId) {
throw new Error(`steps[${index}].providerId required`);
}
if (!prompt) {
throw new Error(`steps[${index}].prompt required`);
}
return {
providerId,
prompt,
...(optionalString(mapped.outputAs) ? { outputAs: optionalString(mapped.outputAs) } : {}),
...(positiveInteger(mapped.timeoutMs, 0) > 0 ? { timeoutMs: positiveInteger(mapped.timeoutMs, 0) } : {}),
};
});
}
function safeStringList(raw) {
if (!Array.isArray(raw)) {
return [];
}
return raw.map((value) => optionalString(value)).filter((value) => value.length > 0);
}
function formatBridgeResultMarkdown(result) {
const lines = ["# Multi-Agent Result", ""];
lines.push(`- Status: ${optionalString(result.status) || "unknown"}`);
lines.push(`- Mode: ${optionalString(result.orchestrationMode) || optionalString(result.mode) || "multi-agent"}`);
const summary = optionalString(result.summary) || optionalString(result.output) || optionalString(result.message);
if (summary) {
lines.push("", "## Summary", "", summary);
}
const steps = Array.isArray(result.steps) ? result.steps : [];
if (steps.length > 0) {
lines.push("", "## Steps", "");
for (const item of steps) {
const step = asRecord(item) ?? {};
lines.push(`- ${optionalString(step.providerId) || "unknown"}: ${optionalString(step.status) || "unknown"}${optionalString(step.error) ? ` (${optionalString(step.error)})` : ""}`);
}
}
lines.push("");
return `${lines.join("\n")}\n`;
}
function bearer(token) {
return token.toLowerCase().startsWith("bearer ") ? token : `Bearer ${token}`;
}
function requiredString(value, message) {
const text = optionalString(value);
if (!text) {
throw new Error(message);
}
return text;
}
function optionalString(value) {
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
return "";
}
const text = String(value).trim();
return text === "<nil>" ? "" : text;
}
function positiveInteger(value, fallback) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
return fallback;
}
return Math.floor(parsed);
}
function asRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
return value;
}