fixed - onboarding and hermes calls

This commit is contained in:
gsknnft
2026-04-01 01:24:23 -04:00
parent c24cf6115e
commit 4332ca22ee
8 changed files with 189 additions and 24 deletions
+164 -3
View File
@@ -285,6 +285,141 @@ function hermesPost(path, body) {
});
}
function hermesGet(path) {
return new Promise((resolve, reject) => {
const urlStr = HERMES_API_URL + path;
let url;
try { url = new URL(urlStr); } catch { reject(new Error(`Invalid URL: ${urlStr}`)); return; }
const transport = url.protocol === "https:" ? https : http;
const headers = {};
if (HERMES_API_KEY) headers["Authorization"] = `Bearer ${HERMES_API_KEY}`;
const req = transport.request(
{ hostname: url.hostname, port: url.port ? parseInt(url.port, 10) : (url.protocol === "https:" ? 443 : 80),
path: url.pathname + (url.search || ""), method: "GET", headers },
resolve
);
req.on("error", reject);
req.end();
});
}
async function readJsonBody(res) {
const chunks = [];
for await (const chunk of res) chunks.push(Buffer.from(chunk));
const raw = Buffer.concat(chunks).toString("utf8");
if (!raw.trim()) return {};
return JSON.parse(raw);
}
function extractOpenAiStyleError(payload, fallbackMessage) {
if (payload && typeof payload === "object") {
const message =
typeof payload?.error?.message === "string"
? payload.error.message.trim()
: "";
if (message) return message;
}
return fallbackMessage;
}
let cachedHermesModels = null;
let cachedHermesModelsAt = 0;
async function fetchHermesModels() {
const now = Date.now();
if (cachedHermesModels && now - cachedHermesModelsAt < 30_000) {
return cachedHermesModels;
}
const res = await hermesGet("/v1/models");
if (res.statusCode >= 400) {
res.resume();
throw new Error(`Hermes models API HTTP ${res.statusCode}`);
}
const payload = await readJsonBody(res);
const models = Array.isArray(payload?.data)
? payload.data
.map((entry) => (typeof entry?.id === "string" ? entry.id.trim() : ""))
.filter(Boolean)
: [];
cachedHermesModels = models;
cachedHermesModelsAt = now;
return models;
}
async function resolveHermesModel(requestedModel) {
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
const normalized = trimmed.includes("/") ? trimmed.split("/").pop().trim() : trimmed;
try {
const models = await fetchHermesModels();
if (models.length === 0) {
return normalized || trimmed || HERMES_MODEL;
}
const candidates = [trimmed, normalized, HERMES_MODEL]
.map((value) => (typeof value === "string" ? value.trim() : ""))
.filter(Boolean);
for (const candidate of candidates) {
const exact = models.find((modelId) => modelId === candidate);
if (exact) return exact;
}
for (const candidate of candidates) {
const suffix = models.find((modelId) => modelId.endsWith(`/${candidate}`));
if (suffix) return suffix;
}
return models[0];
} catch {
return normalized || trimmed || HERMES_MODEL;
}
}
async function completeOneTurn(messages, model, tools) {
const resolvedModel = await resolveHermesModel(model);
const body = { model: resolvedModel, messages, stream: false };
if (tools && tools.length > 0) {
body.tools = tools;
body.tool_choice = "auto";
}
const res = await hermesPost("/v1/chat/completions", body);
const payload = await readJsonBody(res);
if (res.statusCode >= 400) {
throw new Error(
extractOpenAiStyleError(payload, `Hermes API HTTP ${res.statusCode}`)
);
}
const choice = Array.isArray(payload?.choices) ? payload.choices[0] : null;
const message = choice?.message || {};
const textContent =
typeof message?.content === "string"
? message.content
: Array.isArray(message?.content)
? message.content
.map((part) => (typeof part?.text === "string" ? part.text : ""))
.join("")
: "";
const finishReason =
typeof choice?.finish_reason === "string" && choice.finish_reason
? choice.finish_reason
: "stop";
const toolCalls = Array.isArray(message?.tool_calls)
? message.tool_calls.map((tc) => {
let args = {};
const rawArgs = tc?.function?.arguments;
if (typeof rawArgs === "string" && rawArgs.trim()) {
try {
args = JSON.parse(rawArgs);
} catch {
args = { _raw: rawArgs };
}
}
return {
id: typeof tc?.id === "string" ? tc.id : randomId(),
name: typeof tc?.function?.name === "string" ? tc.function.name : "",
args,
};
})
: [];
return { textContent, toolCalls, finishReason, resolvedModel };
}
// ---------------------------------------------------------------------------
// SSE streaming — handles both text deltas and tool calls
// ---------------------------------------------------------------------------
@@ -297,6 +432,8 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
const body = { model, messages, stream: true };
if (tools && tools.length > 0) { body.tools = tools; body.tool_choice = "auto"; }
const resolvedModel = await resolveHermesModel(model);
body.model = resolvedModel;
const res = await hermesPost("/v1/chat/completions", body);
if (res.statusCode >= 400) {
res.resume();
@@ -355,6 +492,15 @@ async function streamOneTurn(messages, model, tools, onTextDelta, abortCheck) {
return { id: tc.id, name: tc.name, args };
});
if (!textContent.trim() && toolCalls.length === 0 && finishReason === "stop") {
const fallback = await completeOneTurn(messages, resolvedModel, tools);
return {
textContent: fallback.textContent,
toolCalls: fallback.toolCalls,
finishReason: fallback.finishReason,
};
}
return { textContent, toolCalls, finishReason };
}
@@ -743,14 +889,15 @@ async function handleMethod(method, params, id, sendEvent) {
const key = typeof p.key === "string" ? p.key : MAIN_SESSION_KEY;
const current = sessionSettings.get(key) || {};
const next = { ...current };
if (p.model !== undefined) next.model = p.model;
if (p.model !== undefined) next.model = typeof p.model === "string" ? p.model.trim() : p.model;
if (p.thinkingLevel !== undefined) next.thinkingLevel = p.thinkingLevel;
if (p.execHost !== undefined) next.execHost = p.execHost;
if (p.execSecurity !== undefined) next.execSecurity = p.execSecurity;
if (p.execAsk !== undefined) next.execAsk = p.execAsk;
sessionSettings.set(key, next);
const resolvedModel = await resolveHermesModel(next.model || HERMES_MODEL);
return resOk(id, { ok: true, key, entry: { thinkingLevel: next.thinkingLevel },
resolved: { model: next.model || HERMES_MODEL, modelProvider: "hermes" } });
resolved: { model: resolvedModel, modelProvider: "hermes" } });
}
case "sessions.reset": {
@@ -896,7 +1043,20 @@ async function handleMethod(method, params, id, sendEvent) {
return resOk(id, { skills: [] });
case "models.list":
return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_AGENT_NAME }] });
try {
const models = await fetchHermesModels();
return resOk(id, {
models: (models.length > 0 ? models : [HERMES_MODEL]).map((modelId) => ({
id: modelId,
name: modelId,
})),
});
} catch {
return resOk(id, { models: [{ id: HERMES_MODEL, name: HERMES_MODEL }] });
}
case "tasks.list":
return resOk(id, { tasks: [] });
// --- Cron jobs ----------------------------------------------------------
@@ -1019,6 +1179,7 @@ function startAdapter() {
"agents.files.get","agents.files.set",
"exec.approvals.get","exec.approvals.set","exec.approval.resolve",
"wake","skills.status","models.list",
"tasks.list",
"cron.list","cron.add","cron.remove","cron.patch","cron.run"],
events: ["chat","presence","heartbeat","cron"] },
snapshot: { health: { agents: allAgents, defaultAgentId: AGENT_ID },
@@ -194,7 +194,7 @@ export function CompanyBuilderModal({
</div>
<h2 className="mt-1 text-lg font-semibold">Design an AI company from one prompt</h2>
<p className="mt-1 text-sm text-white/55">
Uses your connected OpenClaw runtime
Uses your connected runtime
{plannerAgentName ? ` via ${plannerAgentName}.` : "."}
</p>
</div>
@@ -329,7 +329,7 @@ export function CompanyBuilderModal({
Company Actions
</p>
<p className="mt-1 text-[11px] text-white/45">
Generate the org, then create it in OpenClaw.
Generate the org, then create it in your connected runtime.
</p>
</div>
{replacesExistingAgents ? (
@@ -341,7 +341,7 @@ export function CompanyBuilderModal({
) : null}
{!canUseAi ? (
<p className="text-xs text-amber-200/80">
Connect to OpenClaw and keep at least one available planning agent in the fleet
Connect to a runtime and keep at least one available planning agent in the fleet
to use AI suggestions.
</p>
) : null}
@@ -416,7 +416,7 @@ export function CompanyBuilderModal({
<div>
<p className="text-sm font-semibold text-white">Org structure</p>
<p className="text-xs text-white/55">
Edit the team before creating agents in OpenClaw.
Edit the team before creating agents in your connected runtime.
</p>
</div>
<button
@@ -704,7 +704,7 @@ export function CompanyBuilderModal({
{statusLine?.trim() || "Working on your company."}
</p>
<p className="mt-2 text-xs leading-5 text-white/55">
Claw3D is using your OpenClaw runtime right now. Please wait until this finishes.
Claw3D is using your connected runtime right now. Please wait until this finishes.
</p>
<div className="mt-5 flex gap-2">
{Array.from({ length: 4 }, (_, index) => (
@@ -897,7 +897,8 @@ export function CompanyBuilderModal({
What should the company do?
</p>
<p className="mt-2 text-sm text-white/55">
As soon as you submit this, OpenClaw will improve the brief automatically.
As soon as you submit this, Claw3D will improve the brief using your connected
runtime.
</p>
</div>
<button
@@ -923,9 +924,12 @@ export function CompanyBuilderModal({
disabled={busy}
/>
<div className="mt-5 flex items-center justify-between gap-3">
<p className="text-xs text-white/45">
The improved brief becomes the main editable input for generation.
</p>
<div>
<p className="text-xs text-white/45">
The improved brief becomes the main editable input for generation.
</p>
{error ? <p className="mt-2 text-xs text-red-200">{error}</p> : null}
</div>
<button
type="button"
className="inline-flex items-center gap-2 rounded-md bg-amber-500 px-4 py-2 text-sm font-semibold text-[#1a1206] transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:opacity-40"
+1 -1
View File
@@ -311,7 +311,7 @@ const normalizeRole = (value: ParsedCompanyRole, index: number): CompanyBuilderR
export const buildImproveCompanyBriefPrompt = (businessDescription: string) =>
[
"You are helping a user describe the company they want to build inside Claw3D.",
"Rewrite their brief so another OpenClaw agent can generate a clean org structure from it.",
"Rewrite their brief so another connected runtime agent can generate a clean org structure from it.",
"Keep the answer short, concrete, and useful.",
"Return markdown with these sections only:",
"## Company",
+4 -4
View File
@@ -1580,7 +1580,7 @@ export function OfficeScreen({
const runCompanyBuilderAiTask = useCallback(
async (prompt: string, statusText: string) => {
if (status !== "connected") {
throw new Error("Connect to OpenClaw before using the company builder.");
throw new Error("Connect to a runtime before using the company builder.");
}
const livePlannerAgent = resolveCompanyPlanningAgent({
agents: stateRef.current.agents,
@@ -1608,7 +1608,7 @@ export function OfficeScreen({
try {
const improvedBrief = await runCompanyBuilderAiTask(
buildImproveCompanyBriefPrompt(brief),
"Improving your company brief with OpenClaw.",
"Improving your company brief with the connected runtime.",
);
setCompanyBuilderInput((current) => ({
...current,
@@ -1635,7 +1635,7 @@ export function OfficeScreen({
try {
const response = await runCompanyBuilderAiTask(
buildGenerateCompanyPlanPrompt(brief),
"Generating your AI company structure with OpenClaw.",
"Generating your AI company structure with the connected runtime.",
);
const parsedPlan = parseCompanyPlanFromAssistantText(response);
const nextInput: CompanyBuilderInput = {
@@ -1678,7 +1678,7 @@ export function OfficeScreen({
const handleCreateCompanyFromPlan = useCallback(
async (params: { input: CompanyBuilderInput; plan: CompanyBuilderPlan }) => {
if (status !== "connected") {
const message = "Connect to OpenClaw before creating the company.";
const message = "Connect to a runtime before creating the company.";
setCompanyBuilderError(message);
throw new Error(message);
}
@@ -35,7 +35,7 @@ export const CompanyStep = ({
{
icon: Sparkles,
title: "Improve the brief",
description: "Use your connected OpenClaw runtime to sharpen the company prompt.",
description: "Use your connected runtime to sharpen the company prompt.",
},
{
icon: Users,
@@ -45,7 +45,7 @@ export const CompanyStep = ({
{
icon: Wand2,
title: "Create everything",
description: "Write agent files and create the team directly in OpenClaw.",
description: "Write agent files and create the team directly in the connected runtime.",
},
].map(({ icon: Icon, title, description }) => (
<div
@@ -73,7 +73,7 @@ export const CompanyStep = ({
</div>
) : (
<div className="rounded-md border border-amber-500/20 bg-amber-500/10 px-4 py-3 text-xs text-amber-100/80">
Connect to OpenClaw and keep at least one planning agent available to generate the
Connect to a runtime and keep at least one planning agent available to generate the
company with AI.
</div>
)}
@@ -62,7 +62,7 @@ export const CompleteStep = ({
</p>
<p className="max-w-sm text-sm text-white/60">
{companyCreated
? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in OpenClaw and placed into the office.`
? `${companyName?.trim() || "Your company"} is ready. Your new team has been created in the connected runtime and placed into the office.`
: "Your gateway is connected and your agents are ready. Step inside and explore the 3D workspace where your AI team operates."}
</p>
</div>
@@ -32,11 +32,11 @@ export const WelcomeStep = () => (
<p className="text-sm leading-relaxed text-white/80">
Claw3D turns your AI automation into a{" "}
<span className="font-medium text-white">visual workplace</span> an
office where your OpenClaw agents collaborate, code, test, and execute
office where your AI agents collaborate, code, test, and execute
tasks in a shared 3D environment.
</p>
<p className="text-sm text-white/60">
This wizard will help you connect to your OpenClaw gateway and get
This wizard will help you connect to your runtime gateway and get
started in about two minutes.
</p>
</div>
+1 -1
View File
@@ -49,7 +49,7 @@ export const ONBOARDING_STEPS: OnboardingStep[] = [
{
id: "connect",
title: "Connect Your Gateway",
description: "Link to your OpenClaw instance",
description: "Link to your runtime instance",
skippable: false,
},
{