Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions lib/public/js/components/general/agent-defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
import htm from "htm";
import { fetchAgentDefaults, updateAgentDefaults } from "../../lib/api.js";
import { setCached } from "../../lib/api-cache.js";
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
import { ChevronDownIcon } from "../icons.js";
import { ToggleSwitch } from "../toggle-switch.js";
import { showToast } from "../toast.js";

const html = htm.bind(h);

const kAgentDefaultsCacheKey = "/api/agent-defaults";
const kThinkingOptions = [
{ value: "off", label: "Off" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
];

export const AgentDefaults = () => {
const { data, loading } = useCachedFetch(
kAgentDefaultsCacheKey,
fetchAgentDefaults,
{ maxAgeMs: 30000 },
);
const serverDefaults = data?.agentDefaults || null;
const [savingKey, setSavingKey] = useState("");
const [thinking, setThinking] = useState("medium");
const [dreaming, setDreaming] = useState(false);

useEffect(() => {
if (!serverDefaults) return;
if (typeof serverDefaults.thinking === "string") {
setThinking(serverDefaults.thinking);
}
if (typeof serverDefaults.dreaming === "boolean") {
setDreaming(serverDefaults.dreaming);
}
}, [serverDefaults?.thinking, serverDefaults?.dreaming]);

const save = async (key, payload, optimistic) => {
const previous = key === "thinking" ? thinking : dreaming;
optimistic();
setSavingKey(key);
try {
const response = await updateAgentDefaults(payload);
if (!response.ok) {
throw new Error(response.error || "Could not save agent defaults");
}
const next = response.agentDefaults || {};
if (typeof next.thinking === "string") setThinking(next.thinking);
if (typeof next.dreaming === "boolean") setDreaming(next.dreaming);
setCached(kAgentDefaultsCacheKey, { ...(data || {}), ...response });
showToast("Agent defaults saved", "success");
} catch (err) {
if (key === "thinking") setThinking(previous);
else setDreaming(previous);
showToast(err.message || "Could not save agent defaults", "error");
} finally {
setSavingKey("");
}
};

const handleThinkingChange = (event) => {
const nextThinking = String(event.target.value || "");
save("thinking", { thinking: nextThinking }, () => setThinking(nextThinking));
};

const handleDreamingChange = (nextDreaming) => {
save("dreaming", { dreaming: nextDreaming }, () => setDreaming(nextDreaming));
};

const disabled = loading && !serverDefaults;

return html`
<div class="bg-surface border border-border rounded-xl p-4">
<h2 class="card-label mb-3">Agent Defaults</h2>
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<p class="text-sm text-body">Thinking default</p>
<p class="text-xs text-fg-muted">Default reasoning depth for agent calls.</p>
</div>
<div class="relative shrink-0">
<select
value=${thinking}
onchange=${handleThinkingChange}
disabled=${disabled || savingKey === "thinking"}
class="appearance-none bg-field border border-border rounded-lg pl-2.5 pr-9 py-1.5 text-xs text-body ${disabled ||
savingKey === "thinking"
? "opacity-50 cursor-not-allowed"
: ""}"
>
${kThinkingOptions.map(
(option) => html`
<option value=${option.value}>${option.label}</option>
`,
)}
</select>
<${ChevronDownIcon}
className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted"
/>
</div>
</div>
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<p class="text-sm text-body">Dreaming</p>
<p class="text-xs text-fg-muted">Allow the agent to run background ideation between turns.</p>
</div>
<${ToggleSwitch}
checked=${dreaming}
disabled=${disabled || savingKey === "dreaming"}
onChange=${handleDreamingChange}
label=${dreaming ? "On" : "Off"}
/>
</div>
</div>
</div>
`;
};
2 changes: 2 additions & 0 deletions lib/public/js/components/general/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Features } from "../features.js";
import { GeneralDoctorWarning } from "../doctor/general-warning.js";
import { ChevronDownIcon } from "../icons.js";
import { UpdateActionButton } from "../update-action-button.js";
import { AgentDefaults } from "./agent-defaults.js";
import { useGeneralTab } from "./use-general-tab.js";

const html = htm.bind(h);
Expand Down Expand Up @@ -131,6 +132,7 @@ export const GeneralTab = ({
`}
/>
<${Features} onSwitchTab=${onSwitchTab} />
<${AgentDefaults} />
<${Google}
gatewayStatus=${state.gatewayStatus}
onRestartRequired=${onRestartRequired}
Expand Down
34 changes: 34 additions & 0 deletions lib/public/js/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,40 @@ export async function updateAlphaclaw() {
return res.json();
}

export async function fetchAgentDefaults() {
const res = await authFetch("/api/agent-defaults");
const text = await res.text();
let data;
try {
data = text ? JSON.parse(text) : {};
} catch {
throw new Error(text || "Could not parse agent defaults response");
}
if (!res.ok) {
throw new Error(data.error || text || `HTTP ${res.status}`);
}
return data;
}

export async function updateAgentDefaults(payload) {
const res = await authFetch("/api/agent-defaults", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const text = await res.text();
let data;
try {
data = text ? JSON.parse(text) : {};
} catch {
throw new Error(text || "Could not parse agent defaults response");
}
if (!res.ok) {
throw new Error(data.error || text || `HTTP ${res.status}`);
}
return data;
}

export async function fetchSyncCron() {
const res = await authFetch("/api/sync-cron");
const text = await res.text();
Expand Down
107 changes: 107 additions & 0 deletions lib/server/agent-defaults-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const fs = require("fs");
const {
readOpenclawConfig,
resolveOpenclawConfigPath,
writeOpenclawConfig,
} = require("./openclaw-config");

class MalformedOpenclawConfigError extends Error {
constructor(configPath) {
super(
`Refusing to overwrite ${configPath}: file exists but could not be parsed as JSON. Inspect or restore it before changing agent defaults.`,
);
this.code = "MALFORMED_OPENCLAW_CONFIG";
}
}

const kThinkingLevels = Object.freeze(["off", "low", "medium", "high"]);
const kDefaultThinkingLevel = "medium";
const kDefaultDreamingEnabled = false;

const isThinkingLevel = (value) =>
typeof value === "string" && kThinkingLevels.includes(value);

const normalizeAgentDefaults = (rawConfig = {}) => {
const config =
rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig)
? rawConfig
: {};
const raw =
config.agentDefaults &&
typeof config.agentDefaults === "object" &&
!Array.isArray(config.agentDefaults)
? config.agentDefaults
: {};
return {
thinking: isThinkingLevel(raw.thinking) ? raw.thinking : kDefaultThinkingLevel,
dreaming: typeof raw.dreaming === "boolean" ? raw.dreaming : kDefaultDreamingEnabled,
};
};

const readAgentDefaults = ({ fsModule = fs, openclawDir } = {}) => {
const config = readOpenclawConfig({
fsModule,
openclawDir,
fallback: {},
});
return normalizeAgentDefaults(config);
};

const writeAgentDefaults = ({
fsModule = fs,
openclawDir,
thinking,
dreaming,
} = {}) => {
const configPath = resolveOpenclawConfigPath({ openclawDir });
const exists =
typeof fsModule.existsSync === "function"
? fsModule.existsSync(configPath)
: null;
const config = readOpenclawConfig({
fsModule,
openclawDir,
fallback: exists === true ? null : {},
});
if (exists === true && config === null) {
throw new MalformedOpenclawConfigError(configPath);
}
const safeConfig =
config && typeof config === "object" && !Array.isArray(config) ? config : {};
const before = JSON.stringify(safeConfig.agentDefaults || null);
const next = normalizeAgentDefaults(safeConfig);
if (typeof thinking === "string") {
if (!isThinkingLevel(thinking)) {
throw new Error(
`thinking must be one of: ${kThinkingLevels.join(", ")}`,
);
}
next.thinking = thinking;
}
if (typeof dreaming === "boolean") {
next.dreaming = dreaming;
}
safeConfig.agentDefaults = next;
const after = JSON.stringify(safeConfig.agentDefaults);
const changed = before !== after;
if (changed) {
writeOpenclawConfig({
fsModule,
openclawDir,
config: safeConfig,
spacing: 2,
});
}
return { changed, agentDefaults: next };
};

module.exports = {
MalformedOpenclawConfigError,
kThinkingLevels,
kDefaultThinkingLevel,
kDefaultDreamingEnabled,
isThinkingLevel,
normalizeAgentDefaults,
readAgentDefaults,
writeAgentDefaults,
};
1 change: 1 addition & 0 deletions lib/server/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ const SETUP_API_PREFIXES = [
"/api/channels",
"/api/operations",
"/api/nodes",
"/api/agent-defaults",
];

module.exports = {
Expand Down
4 changes: 3 additions & 1 deletion lib/server/doctor/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
kDoctorRunTimeoutMs,
kDoctorStaleThresholdMs,
} = require("./constants");
const { readAgentDefaults } = require("../agent-defaults-config");

const kMaxSnippetLines = 20;

Expand Down Expand Up @@ -214,12 +215,13 @@ const createDoctorService = ({
promptVersion: kDoctorPromptVersion,
});
const gatewayTimeoutMs = kDoctorRunTimeoutMs + 30000;
const { thinking } = readAgentDefaults({ openclawDir: managedRoot });
const gatewayParams = {
agentId: "main",
idempotencyKey: buildDoctorIdempotencyKey(runId),
message: prompt,
sessionKey: buildDoctorSessionKey(runId),
thinking: "medium",
thinking,
timeout: Math.round(kDoctorRunTimeoutMs / 1000),
};
const result = await clawCmd(
Expand Down
58 changes: 58 additions & 0 deletions lib/server/routes/system.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const { buildManagedPaths } = require("../internal-files-migration");
const { readOpenclawConfig } = require("../openclaw-config");
const {
MalformedOpenclawConfigError,
kThinkingLevels,
isThinkingLevel,
readAgentDefaults,
writeAgentDefaults,
} = require("../agent-defaults-config");
const https = require("https");

const registerSystemRoutes = ({
Expand Down Expand Up @@ -576,6 +583,57 @@ const registerSystemRoutes = ({
res.json({ ok: true, syncCron: status });
});

app.get("/api/agent-defaults", (req, res) => {
try {
const agentDefaults = readAgentDefaults({
fsModule: fs,
openclawDir: OPENCLAW_DIR,
});
res.json({ ok: true, agentDefaults, thinkingLevels: kThinkingLevels });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});

app.put("/api/agent-defaults", (req, res) => {
const { thinking, dreaming } = req.body || {};
if (thinking !== undefined && !isThinkingLevel(thinking)) {
return res.status(400).json({
ok: false,
error: `thinking must be one of: ${kThinkingLevels.join(", ")}`,
});
}
if (dreaming !== undefined && typeof dreaming !== "boolean") {
return res
.status(400)
.json({ ok: false, error: "dreaming must be a boolean" });
}
if (thinking === undefined && dreaming === undefined) {
return res.status(400).json({
ok: false,
error: "At least one of thinking or dreaming must be provided",
});
}
try {
const result = writeAgentDefaults({
fsModule: fs,
openclawDir: OPENCLAW_DIR,
thinking,
dreaming,
});
res.json({
ok: true,
changed: result.changed,
agentDefaults: result.agentDefaults,
});
} catch (err) {
if (err instanceof MalformedOpenclawConfigError) {
return res.status(409).json({ ok: false, error: err.message });
}
res.status(500).json({ ok: false, error: err.message });
}
});

app.get("/api/alphaclaw/version", async (req, res) => {
const refresh = String(req.query.refresh || "") === "1";
const status = await alphaclawVersionService.getVersionStatus(refresh);
Expand Down
Loading