openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,492 lines • 61.8 kB
JavaScript
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { a as buildAgentMainSessionKey, b as toAgentStoreSessionKey } from "./session-key-BnWWjqNc.js";
import { d as isSecretRef, h as normalizeSecretInputString } from "./types.secrets-kC0nOetj.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import "./sessions-BdNAJTEP.js";
import { t as SessionManager } from "./session-manager-DO0bDxav.js";
import { i as resolveCliBackendConfig } from "./cli-backends-Bs_a4MW0.js";
import { t as SYSTEM_AGENT_ID } from "./agent-id-D7twbNRG.js";
import { o as prepareSystemAgentRunAdmission } from "./admitted-run-context-CHY5SdVF.js";
import { t as isSystemAgentNavigationOperation } from "./operation-types-LNFFS9G0.js";
import { a as isPersistentSystemAgentOperation, c as isInvalidConfigSetOperation, d as redactSystemAgentConfigPath, l as isSystemAgentSensitiveConfigValue, n as SYSTEM_AGENT_OPERATOR_NAVIGATION_HANDOFF, o as parseSystemAgentOperation, r as describeSystemAgentPersistentOperation } from "./operations-parse-DIiMdt6S.js";
import { t as executeSystemAgentOperation } from "./operations-BAC9TFg6.js";
import { i as resolvePendingOperatorProposal, n as hashSystemAgentOperation, r as resolveOperatorApprovalDecision, t as classifySystemAgentApprovalText } from "./operator-approval-BAOcpwRx.js";
import { n as isSystemAgentInferenceUnavailableError, t as SystemAgentInferenceUnavailableError } from "./inference-error-Dyv9IgLa.js";
import { i as loadSystemAgentOverview, t as formatSystemAgentOnboardingWelcome } from "./overview-BoFF75kz.js";
import { a as resolveSystemAgentVerifiedInferenceRoute, i as resolveSystemAgentExpectedAgentHarnessRuntimeArtifact } from "./verified-inference-D8I0G1w6.js";
import { t as approvalQuestion } from "./dialogue-DF7jf9Tr.js";
import { i as extractAgentRunText } from "./agent-run-result-JlQhspoW.js";
import { a as normalizeCliModel } from "./helpers-D0iW2O3m.js";
import { n as sanitizeWizardStepForClient, r as wizardStepAwaitsInput, t as WizardSession } from "./session-7QOyBCVr.js";
import { a as SYSTEM_AGENT_SYSTEM_PROMPT } from "./assistant-prompts-Cs9_Vt57.js";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
//#region src/system-agent/agent-turn.ts
/**
* OpenClaw is a real agent: same loop, session transcript, and tool pipeline
* as regular agents — restricted to the single ring-zero `openclaw` tool.
* Embedded runtimes enforce that restriction with toolsAllow. CLI harnesses
* must explicitly support per-run native-tool selection, then receive the tool
* over a dedicated stdio MCP server that replaces the normal bundle surface.
* Turns share one persistent session so the conversation has genuine
* multi-turn memory. Inference setup must succeed before this runner is entered.
*/
const AGENT_TURN_TIMEOUT_MS = 12e4;
const SYSTEM_AGENT_TOOL_NAME = "openclaw";
function createSystemAgentSession(verifiedInference) {
if (!verifiedInference) throw new SystemAgentInferenceUnavailableError("agent-turn");
return {
sessionId: `openclaw-${randomUUID()}`,
verifiedInference,
proposalRef: {}
};
}
async function ensureSystemAgentDirs() {
const base = path.join(resolveStateDir(), "openclaw");
const workspaceDir = path.join(base, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
return { workspaceDir };
}
async function cleanupSystemAgentSession(session) {
delete session.cliSession;
delete session.sessionManager;
}
function clearSystemAgentCliSession(session) {
delete session.cliSession;
}
function clearFailedSystemAgentSessionState(session) {
session.proposalRef.current = void 0;
session.proposalRef.operation = void 0;
clearSystemAgentCliSession(session);
}
function throwSystemAgentInferenceUnavailable(params) {
clearFailedSystemAgentSessionState(params.session);
throw new SystemAgentInferenceUnavailableError("agent-turn", params.failures);
}
function cliRouteKey(route, backend) {
return JSON.stringify({
provider: route.provider,
backendId: backend?.id ?? route.provider,
modelLabel: route.modelLabel,
configuredModel: route.model,
model: backend ? normalizeCliModel(route.model, backend.config) : route.model,
authProfileId: route.authProfileId ?? "",
agentDir: path.resolve(route.agentDir),
backend: backend ? {
pluginId: backend.pluginId,
modelProvider: backend.modelProvider,
config: backend.config,
bundleMcp: backend.bundleMcp,
bundleMcpMode: backend.bundleMcpMode,
authEpochMode: backend.authEpochMode,
nativeToolMode: backend.nativeToolMode,
toolAvailabilityEnforcement: backend.toolAvailabilityEnforcement,
sideQuestionToolMode: backend.sideQuestionToolMode
} : null
});
}
function resolveSystemAgentCliBackend(route) {
const backend = resolveCliBackendConfig(route.provider, route.runConfig, { agentId: SYSTEM_AGENT_ID });
if (!backend) return null;
const { liveSession: _liveSession, ...config } = backend.config;
return {
...backend,
config
};
}
function resolveSystemAgentCliToolAvailability(backend) {
if (backend?.nativeToolMode === "none") return;
if (backend?.nativeToolMode === "selectable" && (backend.toolAvailabilityEnforcement === "execution-args" && backend.resolveExecutionArgs || backend.toolAvailabilityEnforcement === "prepare-execution" && backend.prepareExecution)) return {
native: [],
openClaw: [SYSTEM_AGENT_TOOL_NAME]
};
const backendId = backend?.id ?? "unknown";
throw new Error(`CLI backend ${backendId} cannot enforce OpenClaw's exact tool availability`);
}
/**
* CLI harnesses run the openclaw tool in a stdio MCP subprocess, so the
* in-process proposalRef/directiveRef cannot be shared with the host. Mirror
* the tool's transitions from the harness tool events instead: a denial
* registers the exact-operation hash, a mismatch voids it, an executed
* mutation consumes it, and directive actions replay the interactive handoff —
* same lifecycle as system-agent-tool.ts enforces.
*/
async function mirrorSystemAgentToolStateFromEvents(params) {
const [{ onAgentEventForRun }, { extractToolResultText }, { resolveSystemAgentProposalTransition, resolveSystemAgentDirectiveTransition }] = await Promise.all([
import("./agent-events-BmgMD6UA.js"),
import("./embedded-agent-tool-results-CuhcX8mD.js"),
import("./system-agent-tool-CgJexaQa.js")
]);
return onAgentEventForRun(params.runId, (evt) => {
if (evt.runId !== params.runId || evt.stream !== "tool" || evt.data.phase !== "result") return;
const name = typeof evt.data.name === "string" ? evt.data.name : "";
if (name !== "openclaw" && !name.endsWith("__openclaw")) return;
const args = typeof evt.data.args === "object" && evt.data.args !== null ? evt.data.args : {};
const resultText = extractToolResultText(evt.data.result) ?? "";
const transition = resolveSystemAgentProposalTransition({
args,
resultText
});
if (transition) {
params.proposalRef.current = transition.proposal;
params.proposalRef.operation = transition.operation;
}
const directive = resolveSystemAgentDirectiveTransition({
args,
resultText
});
if (directive && params.directiveRef.current?.kind !== "approved-operation") params.directiveRef.current = directive;
});
}
/**
* Run one OpenClaw turn through the embedded agent loop. Route, runner, and
* output failures are typed so callers may try another inference path without
* mistaking the failure for deterministic setup authority.
*/
async function runSystemAgentTurnWithDeps(params, deps = {}) {
const binding = params.session.verifiedInference;
if (!binding) return throwSystemAgentInferenceUnavailable({ session: params.session });
let plan;
try {
plan = await resolveSystemAgentVerifiedInferenceRoute(binding, deps);
} catch (error) {
return throwSystemAgentInferenceUnavailable({
session: params.session,
failures: [error]
});
}
if (!plan) return throwSystemAgentInferenceUnavailable({ session: params.session });
let expectedAgentHarnessRuntimeArtifact;
try {
expectedAgentHarnessRuntimeArtifact = resolveSystemAgentExpectedAgentHarnessRuntimeArtifact(binding);
} catch (error) {
return throwSystemAgentInferenceUnavailable({
session: params.session,
failures: [error]
});
}
let workspaceDir;
try {
({workspaceDir} = await ensureSystemAgentDirs());
} catch (error) {
return throwSystemAgentInferenceUnavailable({
session: params.session,
failures: [error]
});
}
const runId = `openclaw-turn-${randomUUID()}`;
const sessionManager = params.session.sessionManager ?? SessionManager.inMemory(workspaceDir);
params.session.sessionManager = sessionManager;
const preparedRunAdmission = prepareSystemAgentRunAdmission(plan.runConfig, runId, SYSTEM_AGENT_ID, "system-agent.turn");
const policySessionKey = buildAgentMainSessionKey({ agentId: SYSTEM_AGENT_ID });
const shared = {
sessionId: params.session.sessionId,
sessionKey: toAgentStoreSessionKey({
agentId: SYSTEM_AGENT_ID,
requestKey: params.session.sessionId
}),
agentId: SYSTEM_AGENT_ID,
trigger: "manual",
sessionFile: `in-memory:${params.session.sessionId}`,
sessionManager,
workspaceDir,
config: plan.runConfig,
prompt: params.input,
timeoutMs: AGENT_TURN_TIMEOUT_MS,
thinkLevel: "off",
runId,
messageChannel: "openclaw",
messageProvider: "openclaw",
disableTrajectory: true
};
const directiveRef = {};
const systemAgentTool = {
agentId: plan.agentId,
surface: params.surface,
approvalArmed: params.approvalArmed,
...params.operatorApprovalOnly ? { operatorApprovalOnly: true } : {},
proposalRef: params.session.proposalRef,
directiveRef
};
try {
let result;
if (plan.runner === "cli") {
const backend = resolveSystemAgentCliBackend(plan);
const cliToolAvailability = resolveSystemAgentCliToolAvailability(backend);
const routeKey = cliRouteKey(plan, backend);
const previousBinding = params.session.cliSession?.routeKey === routeKey ? params.session.cliSession.binding : void 0;
if (!previousBinding) clearSystemAgentCliSession(params.session);
const runCli = deps.runCliAgent ?? (await import("./cli-runner-msdgHSKR.js")).runCliAgent;
const stopToolStateMirror = await mirrorSystemAgentToolStateFromEvents({
runId,
proposalRef: params.session.proposalRef,
directiveRef
});
try {
result = await runCli({
...shared,
preparedRunAdmission,
provider: plan.provider,
model: plan.model,
agentDir: plan.agentDir,
...plan.authProfileId ? { authProfileId: plan.authProfileId } : {},
extraSystemPrompt: SYSTEM_AGENT_SYSTEM_PROMPT,
extraSystemPromptStatic: SYSTEM_AGENT_SYSTEM_PROMPT,
systemAgentTool,
...cliToolAvailability ? { cliToolAvailability } : {},
...previousBinding ? { cliSessionBinding: previousBinding } : {},
runtimePolicySessionKey: policySessionKey,
disableCliLiveSession: true,
cleanupCliLiveSessionOnRunEnd: true
});
} finally {
stopToolStateMirror();
}
const agentMeta = result.meta?.agentMeta;
if (agentMeta?.clearCliSessionBinding || !agentMeta?.cliSessionBinding?.sessionId) clearSystemAgentCliSession(params.session);
else if (agentMeta?.cliSessionBinding?.sessionId) params.session.cliSession = {
routeKey,
binding: agentMeta.cliSessionBinding
};
} else {
clearSystemAgentCliSession(params.session);
result = await (deps.runEmbeddedAgent ?? (await import("./embedded-agent-CUhbhIMN.js")).runEmbeddedAgent)({
...shared,
preparedRunAdmission,
extraSystemPrompt: SYSTEM_AGENT_SYSTEM_PROMPT,
toolsAllow: ["openclaw"],
systemAgentTool,
disableMessageTool: true,
provider: plan.provider,
model: plan.model,
agentDir: plan.agentDir,
agentHarnessRuntimeOverride: plan.agentHarnessRuntimeOverride,
sandboxSessionKey: policySessionKey,
...expectedAgentHarnessRuntimeArtifact ? { expectedAgentHarnessRuntimeArtifact } : {},
...plan.authProfileId ? {
authProfileId: plan.authProfileId,
authProfileIdSource: "user"
} : {}
});
}
if (params.session.verifiedInference !== binding) throw new SystemAgentInferenceUnavailableError("agent-turn");
if (!await resolveSystemAgentVerifiedInferenceRoute(binding, deps)) throw new SystemAgentInferenceUnavailableError("agent-turn");
const text = extractAgentRunText(result)?.trim();
if (!text) throw new SystemAgentInferenceUnavailableError("agent-turn");
return {
text,
modelLabel: plan.modelLabel,
...directiveRef.current ? { directive: directiveRef.current } : {}
};
} catch (error) {
const failures = error instanceof SystemAgentInferenceUnavailableError ? [...error.failures] : [error];
return throwSystemAgentInferenceUnavailable({
session: params.session,
failures
});
} finally {
preparedRunAdmission.close();
}
}
const runSystemAgentTurn = (params) => runSystemAgentTurnWithDeps(params);
if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.systemAgentTurnTestApi")] = { runSystemAgentTurnWithDeps };
//#endregion
//#region src/system-agent/chat-turn-router.ts
const log$1 = createSubsystemLogger("system-agent/chat-engine");
function createCaptureRuntime() {
const lines = [];
return {
log: (...args) => lines.push(args.join(" ")),
error: (...args) => lines.push(args.join(" ")),
exit: (code) => {
throw new Error(`OpenClaw operation exited with code ${String(code)}`);
},
read: () => lines.join("\n").trim()
};
}
function formatOperationError(error) {
return `That did not go through: ${error instanceof Error ? error.message : String(error)}`;
}
function redactSensitiveCommandText(text) {
const operation = parseSystemAgentOperation(text);
if (isInvalidConfigSetOperation(operation)) return "config set <invalid path> <redacted secret>";
if (operation.kind === "config-set") {
const displayPath = redactSystemAgentConfigPath(operation.path);
if (displayPath !== operation.path || isSystemAgentSensitiveConfigValue(operation.path, operation.value)) return `config set ${displayPath} <redacted secret>`;
}
if (operation.kind === "config-set-ref") return `config set-ref ${redactSystemAgentConfigPath(operation.path)} <redacted reference>`;
return text;
}
function formatPendingOperationForAssistant(operation) {
const description = describeSystemAgentPersistentOperation(operation);
return operation.kind === "setup" ? `${description}. Exact setup JSON: ${JSON.stringify(operation)}. Keep the verified model unless the user explicitly asks to leave OpenClaw and reconfigure inference.` : description;
}
function preservePendingSetupModel(pending, operation) {
if (pending?.kind !== "setup" || operation.kind !== "setup") return operation;
const pendingModel = pending.model?.trim();
const requestedModel = operation.model?.trim();
const withAgentName = {
...operation,
...operation.agentName ? {} : pending.agentName ? { agentName: pending.agentName } : {}
};
if (requestedModel && requestedModel !== pendingModel) return withAgentName;
return {
...withAgentName,
...requestedModel ? {} : pendingModel ? { model: pendingModel } : {}
};
}
var ChatTurnRouter = class {
constructor(options, dependencies, agentSession, wizard, callbacks) {
this.options = options;
this.dependencies = dependencies;
this.agentSession = agentSession;
this.wizard = wizard;
this.callbacks = callbacks;
this.pending = null;
this.awaitingSetupChannel = false;
}
propose(operation) {
this.clearPendingProposals();
this.pending = this.recordCreateAgentRequester(operation);
return describeSystemAgentPersistentOperation(this.pending);
}
getPendingOperatorProposal() {
const proposal = resolvePendingOperatorProposal(this.pending, this.agentSession.proposalRef);
if (!proposal) return null;
const operation = this.recordCreateAgentRequester(proposal.operation);
return operation === proposal.operation ? proposal : {
operation,
hash: hashSystemAgentOperation(operation)
};
}
async resolveOperatorApproval(decision, proposalHash, beforePersistentApply) {
return await resolveOperatorApprovalDecision({
decision,
proposalHash,
getProposal: () => this.getPendingOperatorProposal(),
clear: () => this.clearPendingProposals(),
apply: async (operation) => {
this.proposalResolution = "approved";
return await this.applyApprovedPersistentOperation(operation, beforePersistentApply);
},
denied: () => {
this.proposalResolution = "declined";
return {
text: "Denied. No change.",
action: "none",
applied: false
};
}
});
}
clearForInferenceLoss() {
this.clearPendingProposals();
this.proposalResolution = void 0;
this.awaitingSetupChannel = false;
this.lastSensitiveChannel = void 0;
}
async answerWizard(result) {
const answer = await result;
return {
...answer,
text: await this.finishWizardText(answer)
};
}
async resolveTurn(text, options) {
if (this.wizard.active) {
const result = await this.wizard.resolveReply(text);
return {
text: await this.finishWizardText(result),
action: "none"
};
}
const trimmed = text.trim();
if (!trimmed) return {
text: "Tiny claw tap: tell me what you want — setup, repair, channels, anything config.",
action: "none"
};
if (/^(quit|exit)$/i.test(trimmed)) return {
text: "OpenClaw retracts into shell. Bye.",
action: "exit"
};
if (this.awaitingSetupChannel) {
if (/^(cancel|abort|stop)$/i.test(trimmed)) {
this.awaitingSetupChannel = false;
return {
text: "Channel wizard handoff cancelled.",
action: "none"
};
}
if (!/^[a-z0-9_-]+$/i.test(trimmed)) return {
text: "Reply with one channel id, such as `slack` or `telegram`, or say `cancel`.",
action: "none"
};
this.awaitingSetupChannel = false;
return await this.runOperation({
kind: "open-setup",
target: "channels",
channel: trimmed.toLowerCase()
}, void 0);
}
if (this.options.operatorApprovalOnly && this.getPendingOperatorProposal()) return {
text: "Approval pending. Human must decide in OpenClaw UI.",
action: "none"
};
const typed = parseSystemAgentOperation(text);
if (isInvalidConfigSetOperation(typed)) return {
text: typed.message,
action: "none"
};
if (typed.kind === "config-set" || typed.kind === "config-set-ref" || typed.kind === "config-get" || typed.kind === "config-schema") return await this.runOperation(typed, void 0);
if (isSystemAgentNavigationOperation(typed)) return await this.runOperation(typed, void 0);
const intent = this.options.operatorApprovalOnly ? "other" : await this.classifyApprovalIntent(text);
if (this.pending) {
if (intent === "approve") {
await this.callbacks.requireVerifiedInference();
return await this.applyPendingProposal(this.pending);
}
if (intent === "decline") {
this.clearPendingProposals();
this.proposalResolution = "declined";
return {
text: "Skipped. No barnacles on config today.",
action: "none"
};
}
}
if (intent === "decline") {
this.agentSession.proposalRef.current = void 0;
this.agentSession.proposalRef.operation = void 0;
}
return await this.resolveAssistantTurn(text, this.options.operatorApprovalOnly ? false : intent === "approve", options?.uiContext);
}
async classifyApprovalIntent(text) {
if (!(this.pending !== null || this.agentSession.proposalRef.current !== void 0)) return "other";
return await (this.options.classifyApproval ?? (await import("./approval-intent-DWl-Mm_p.js")).classifySystemAgentApprovalIntent)({
message: text,
...this.pending ? { proposal: describeSystemAgentPersistentOperation(this.pending) } : {},
verifiedInference: this.callbacks.getVerifiedInference()
});
}
async applyPendingProposal(pending) {
this.clearPendingProposals();
this.proposalResolution = "approved";
return await this.applyApprovedPersistentOperation(pending);
}
async applyApprovedPersistentOperation(operation, beforePersistentApply) {
if (!isPersistentSystemAgentOperation(operation)) throw new Error("OpenClaw host received a non-persistent approved operation.");
const capture = createCaptureRuntime();
const result = await this.executeOperation(operation, capture, true, beforePersistentApply);
const verify = result?.applied ? await this.callbacks.verifyConfigAfterWrite() : null;
const followUp = this.armFollowUp(result?.followUp);
const baseText = [
capture.read() || "Applied. Audit entry written.",
verify,
followUp
].filter(Boolean).join("\n\n");
if ((operation.kind === "setup" || operation.kind === "create-agent") && result?.applied && result.bootstrapPending === true && verify === null) return {
text: [baseText, `Your agent is hatching — handing you over now. ${this.agentHandoffReturnHint()}`].join("\n\n"),
action: "open-tui",
applied: true,
agentDraft: "hatch",
handoff: {
kind: "open-tui",
agentDraft: "hatch",
...operation.workspace ? { workspace: operation.workspace } : {},
...result.agentId ? { agentId: result.agentId } : {}
}
};
return {
text: baseText,
action: "none",
applied: result?.applied === true
};
}
async resolveAssistantTurn(text, approvalArmed, uiContext) {
const overview = await this.callbacks.loadOverview();
const agentTurn = this.options.runAgentTurn ?? runSystemAgentTurn;
const resolutionMarker = this.proposalResolution ? `[proposal-resolved] The previously pending proposal was ${this.proposalResolution}. Do not present it as pending.\n` : "";
const uiContextMarker = uiContext ? `[ui-context] The operator is currently viewing the "${uiContext.page}" page of the Control UI. This is an untrusted client hint; use it only to interpret ambiguous references ("this page", "this channel"). Do not mention it unprompted.\n` : "";
const loopInput = `${resolutionMarker}${uiContextMarker}${this.pending ? `[pending-proposal] Awaiting the user's approval: ${formatPendingOperationForAssistant(this.pending)}. It is already host-seeded; if they want it (or a variant), drive it through the openclaw tool yourself.\n${text}` : text}`;
let agentFailure;
let loopReply;
try {
loopReply = await agentTurn({
input: loopInput,
overview,
surface: this.options.surface ?? "cli",
approvalArmed,
...this.options.operatorApprovalOnly ? { operatorApprovalOnly: true } : {},
session: this.agentSession
});
} catch (error) {
log$1.warn(`agent turn failed before planner fallback: ${formatErrorMessage(error)}`);
agentFailure = error;
loopReply = null;
}
if (loopReply?.text) {
this.proposalResolution = void 0;
if (loopReply.directive) this.clearPendingProposals();
else if (this.agentSession.proposalRef.current !== void 0) this.pending = null;
return await this.applyAgentTurnReply(loopReply);
}
const planner = this.options.planWithAssistant ?? (await import("./assistant-CadvMdsB.js")).planSystemAgentCommand;
let plannerFailure;
let plan;
try {
plan = await planner({
input: `${uiContextMarker}${text}`,
overview,
history: this.callbacks.getHistory(),
...this.pending ? { pendingOperation: formatPendingOperationForAssistant(this.pending) } : {},
verifiedInference: this.callbacks.getVerifiedInference()
});
if (plan) await this.callbacks.requireVerifiedInference();
} catch (error) {
plannerFailure = error;
plan = null;
}
if (!plan) throw new SystemAgentInferenceUnavailableError("conversation", [agentFailure, plannerFailure].filter((failure) => failure !== void 0));
const replyText = plan.reply ?? "";
if (!plan.command) {
if (!replyText.trim()) throw new SystemAgentInferenceUnavailableError("planner", [agentFailure]);
return {
text: replyText,
action: "none"
};
}
const operation = preservePendingSetupModel(this.pending, parseSystemAgentOperation(plan.command));
if (operation.kind === "none") {
if (!replyText.trim()) throw new SystemAgentInferenceUnavailableError("planner", [agentFailure]);
return {
text: replyText,
action: "none"
};
}
const provenance = `(${plan.modelLabel ?? "model"} → \`${plan.command}\`)`;
const executed = await this.runOperation(operation, provenance);
return {
...executed,
text: [replyText, executed.text].filter(Boolean).join("\n\n")
};
}
async applyAgentTurnReply(loopReply) {
await this.callbacks.requireVerifiedInference();
const directive = loopReply.directive;
if (!directive) return {
text: loopReply.text,
action: "none"
};
const reply = directive.kind === "approved-operation" ? await this.applyApprovedPersistentOperation(directive.operation) : await this.runOperation(directive, void 0);
return {
...reply,
text: directive.kind === "open-tui" && reply.action === "open-tui" ? loopReply.text : [loopReply.text, reply.text].filter(Boolean).join("\n\n")
};
}
async runOperation(operation, provenance) {
const recordedOperation = this.recordCreateAgentRequester(operation);
await this.callbacks.requireVerifiedInference();
if (this.options.operatorApprovalOnly && isSystemAgentNavigationOperation(recordedOperation)) return {
text: SYSTEM_AGENT_OPERATOR_NAVIGATION_HANDOFF,
action: "none"
};
if (recordedOperation.kind === "open-tui") {
this.clearPendingProposals();
return {
text: `Opening a chat with your agent. ${this.agentHandoffReturnHint()}`,
action: "open-tui",
handoff: recordedOperation
};
}
if (recordedOperation.kind === "model-accounts") {
this.clearPendingProposals();
return {
text: this.options.surface === "gateway" ? "Opening Settings → Profile → Connected accounts. Check the Gateway, person, and Personal scope, then sign in or select a saved account. Nothing has changed yet; never paste credentials into this conversation." : "Run `openclaw models accounts list` to see your personal accounts, or `openclaw models accounts login <provider>` for protected sign-in. Check the Gateway and person shown before signing in. You can also use Settings → Profile → Connected accounts in the Control UI. Nothing has changed; never paste credentials into this conversation.",
action: "none",
...this.options.surface === "gateway" ? { handoff: recordedOperation } : {}
};
}
if (recordedOperation.kind === "open-setup") {
this.clearPendingProposals();
if (this.options.surface === "gateway") return {
text: "Open Settings to change your model or connect a channel. To change providers from a shell, run `openclaw onboard` on the machine running OpenClaw.",
action: "none"
};
if (![
"channels",
"search",
"gateway"
].includes(recordedOperation.target)) return {
text: "Setup can replace the inference route powering this session. Exit OpenClaw and run `openclaw onboard`; it saves only a route that passes a live test. Then start OpenClaw again.",
action: "none"
};
let handoff = recordedOperation;
if (handoff.target === "channels" && !handoff.channel) {
if (!this.lastSensitiveChannel) {
this.awaitingSetupChannel = true;
return {
text: "Which channel should I open in the masked terminal wizard?",
action: "none"
};
}
handoff = {
...handoff,
channel: this.lastSensitiveChannel
};
this.lastSensitiveChannel = void 0;
}
this.awaitingSetupChannel = false;
return {
text: `Opening the ${handoff.target === "channels" ? `${handoff.channel ?? "channel"} setup` : handoff.target === "search" ? "web search setup" : "Gateway setup"} wizard.`,
action: "open-setup",
handoff
};
}
if (recordedOperation.kind === "channel-setup") return await this.startWizard(this.wizard.startChannel(recordedOperation.channel));
if (recordedOperation.kind === "skills-setup") return await this.startWizard(this.wizard.startSkills());
if (recordedOperation.kind === "search-setup") return await this.startWizard(this.wizard.startSearch());
if (recordedOperation.kind === "gateway-config-setup") return await this.startWizard(this.wizard.startGateway());
if (recordedOperation.kind === "memory-import") return await this.startWizard(this.wizard.startMemoryImport());
if (recordedOperation.kind === "model-setup") return this.startModelSetup();
const capture = createCaptureRuntime();
if (isPersistentSystemAgentOperation(recordedOperation) && !this.options.yes) {
this.clearPendingProposals();
await executeSystemAgentOperation(recordedOperation, capture, {
approved: false,
operatorApprovalOnly: this.options.operatorApprovalOnly,
deps: this.commandDeps()
});
this.pending = recordedOperation;
return {
text: [
provenance,
capture.read(),
this.options.operatorApprovalOnly ? void 0 : approvalQuestion(recordedOperation)
].filter(Boolean).join("\n\n"),
action: "none"
};
}
const result = await this.executeOperation(recordedOperation, capture, this.options.yes === true || !isPersistentSystemAgentOperation(recordedOperation));
const verify = result?.applied ? await this.callbacks.verifyConfigAfterWrite() : null;
const followUp = this.armFollowUp(result?.followUp);
const reply = [
provenance,
capture.read(),
verify,
followUp
].filter(Boolean).join("\n\n");
if (result?.exitsInteractive === true) return {
text: reply,
action: "exit"
};
return {
text: reply,
action: "none"
};
}
async executeOperation(operation, capture, approved, beforePersistentApply) {
try {
const execute = this.dependencies.executeOperation ?? executeSystemAgentOperation;
if (approved) await this.callbacks.requirePersistentApplyInference(capture);
return await execute(operation, capture, {
approved,
deps: this.commandDeps(),
beforePersistentApply,
onVerifiedInferenceChanged: this.callbacks.rebindVerifiedInference
});
} catch (error) {
if (isSystemAgentInferenceUnavailableError(error)) throw error;
capture.error(formatOperationError(error));
return;
}
}
async startWizard(result) {
this.lastSensitiveChannel = void 0;
this.clearPendingProposals();
const resolved = await result;
if (resolved.sensitiveChannel) this.lastSensitiveChannel = resolved.sensitiveChannel;
return {
text: await this.finishWizardText(resolved),
action: "none"
};
}
async finishWizardText(result) {
const verify = result.configWritten ? await this.callbacks.verifyConfigAfterWrite() : null;
return [result.text, verify].filter(Boolean).join("\n");
}
startModelSetup() {
this.clearPendingProposals();
return {
text: ["Changing provider credentials would replace the inference route powering this session.", "Stop the OpenClaw host through whatever started it. Run `openclaw onboard` on the machine running OpenClaw: it stages credentials, live-tests the new route, and saves only a passing setup. Then restart the host and return to OpenClaw."].join("\n"),
action: "none"
};
}
commandDeps() {
return {
...this.options.deps,
loadOverview: this.callbacks.loadOverview,
...this.options.surface ? { setupSurface: this.options.surface } : {}
};
}
agentHandoffReturnHint() {
return this.options.surface === "gateway" ? "You can return through Settings → Ask OpenClaw." : "Use /openclaw to come back.";
}
clearPendingProposals() {
this.pending = null;
this.agentSession.proposalRef.current = void 0;
this.agentSession.proposalRef.operation = void 0;
}
recordCreateAgentRequester(operation) {
const requesterAgentId = this.options.requesterAgentId?.trim();
if (operation.kind !== "create-agent" || !requesterAgentId || operation.requesterAgentId === requesterAgentId) return operation;
return {
...operation,
requesterAgentId
};
}
armFollowUp(operation) {
return operation?.kind === "model-setup" ? ["No usable inference route is configured, so OpenClaw cannot continue.", "Run `openclaw onboard` on the machine running OpenClaw; it saves only a route that passes a live test."].join("\n") : null;
}
};
//#endregion
//#region src/system-agent/chat-wizard-host.ts
const log = createSubsystemLogger("system-agent/chat-wizard-host");
const WIZARD_CANCEL_HINT = "Say `cancel` to stop this setup.";
let hostedRuntimePromise;
function loadHostedRuntime() {
return hostedRuntimePromise ??= import("./hosted-setup.runtime.js");
}
function formatWizardOptions(step) {
return (step.options ?? []).map((option, index) => {
const hint = option.hint ? ` — ${option.hint}` : "";
return `${index + 1}. ${option.label}${hint}`;
});
}
function wizardStepChatQuestion(step) {
if (!step) return;
if (step.type === "confirm") {
const yesRecommended = step.initialValue !== false;
return {
id: step.id,
header: step.title ?? "Confirm",
question: step.message ?? "Continue?",
options: [{
label: "Yes",
reply: "yes",
...yesRecommended ? { recommended: true } : {}
}, {
label: "No",
reply: "no",
...!yesRecommended ? { recommended: true } : {}
}]
};
}
if (step.type !== "select") return;
const options = step.options ?? [];
if (options.length < 2 || options.length > 4) return;
return {
id: step.id,
header: step.title ?? "Choose one",
question: step.message ?? "Choose one.",
options: options.map((option) => {
const mapped = { label: option.label };
if (option.hint) mapped.description = option.hint;
if (step.initialValue !== void 0 && option.value === step.initialValue) mapped.recommended = true;
return mapped;
})
};
}
function renderWizardStep(step) {
const lines = [];
if (step.title) lines.push(`**${step.title}**`);
if (step.message) lines.push(step.message);
switch (step.type) {
case "select":
lines.push(...formatWizardOptions(step), "Reply with a number.");
break;
case "multiselect":
lines.push(...formatWizardOptions(step), "Reply with numbers (e.g. 1,3) or `none`.");
break;
case "confirm":
lines.push("Reply yes or no.");
break;
case "text":
if (step.placeholder) lines.push(`(e.g. ${step.placeholder})`);
lines.push("Type your answer.");
}
return lines.filter(Boolean).join("\n");
}
function parseWizardAnswer(step, text) {
const trimmed = text.trim();
if (step.type === "confirm") {
const intent = classifySystemAgentApprovalText(trimmed);
return intent === "approve" ? { value: true } : intent === "decline" ? { value: false } : null;
}
if (step.type === "text") return { value: trimmed };
const options = step.options ?? [];
const matchOption = (token) => {
if (/^\d+$/.test(token)) {
const index = Number(token);
if (Number.isSafeInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
}
const lower = token.toLowerCase();
return options.find((option) => option.label.toLowerCase() === lower || typeof option.value === "string" && option.value.toLowerCase() === lower);
};
if (step.type === "select") {
const option = matchOption(trimmed);
return option ? { value: option.value } : null;
}
if (step.type === "multiselect") {
if (/^none$/i.test(trimmed)) return { value: [] };
const values = [];
for (const token of trimmed.split(/[\s,]+/).filter(Boolean)) {
const option = matchOption(token);
if (!option) return null;
values.push(option.value);
}
return { value: values };
}
return { value: step.type === "action" ? true : void 0 };
}
function formatStructuredWizardAnswerForHistory(step, value) {
if (step.sensitive === true) return "<redacted secret>";
if (step.type === "text") return [
"string",
"number",
"boolean",
"bigint"
].includes(typeof value) ? String(value) : "<wizard answer>";
if (step.type === "confirm") return typeof value === "boolean" ? value ? "Yes" : "No" : "<wizard answer>";
if (step.type === "select") return step.options?.find((option) => Object.is(option.value, value))?.label ?? "<wizard answer>";
if (step.type === "multiselect") {
if (!Array.isArray(value)) return "<wizard answer>";
if (value.length === 0) return "None";
const labels = value.map((entry) => step.options?.find((option) => Object.is(option.value, entry))?.label);
return labels.every((label) => label !== void 0) ? labels.join(", ") : "<wizard answer>";
}
return "Continue";
}
var SystemAgentWizardAnswerError = class extends Error {};
var ChatWizardHost = class {
constructor(options) {
this.options = options;
this.bridge = null;
}
get active() {
return this.bridge !== null;
}
get sensitiveInputPending() {
return this.bridge?.step?.sensitive === true;
}
dispose() {
this.bridge?.session.cancel();
this.bridge = null;
}
decorateReply(reply) {
const step = this.bridge?.step ?? null;
const completedReply = reply.text && step && wizardStepAwaitsInput(step) ? {
...reply,
text: `${reply.text}\n${WIZARD_CANCEL_HINT}`
} : reply;
const question = wizardStepChatQuestion(step);
const clientStep = step ? sanitizeWizardStepForClient(step) : null;
return {
...completedReply,
...step?.sensitive === true ? { sensitive: true } : {},
...this.bridge ? { wizardInputPending: true } : {},
...question ? { question } : {},
...clientStep ? { step: clientStep } : {}
};
}
async answer(answer) {
const bridge = this.bridge;
const step = bridge?.step;
if (!bridge || !step) throw new SystemAgentWizardAnswerError("No hosted wizard is awaiting an answer.");
if (answer.stepId !== step.id) throw new SystemAgentWizardAnswerError("The hosted wizard answer targets a stale step.");
const validationError = await bridge.session.answer(step.id, answer.value);
return {
...validationError ? {
text: [validationError, renderWizardStep(step)].join("\n\n"),
configWritten: false
} : await this.pump(),
userHistoryText: formatStructuredWizardAnswerForHistory(step, answer.value)
};
}
async cancel(cancel) {
const bridge = this.bridge;
const step = bridge?.step;
if (!bridge || !step) throw new SystemAgentWizardAnswerError("No hosted wizard is awaiting cancellation.");
if (cancel.stepId !== step.id) throw new SystemAgentWizardAnswerError("The hosted wizard cancel targets a stale step.");
if (!bridge.session.cancel()) throw new SystemAgentWizardAnswerError("The hosted wizard cannot be cancelled right now.");
return {
...await this.pump(),
userHistoryText: "Cancel"
};
}
async resolveReply(text) {
const bridge = this.bridge;
if (!bridge) return {
text: "",
configWritten: false
};
if (/^(cancel|abort|stop|quit|exit)$/i.test(text.trim())) {
bridge.session.cancel();
return await this.pump();
}
const step = bridge.step;
if (!step) return await this.pump();
const answer = parseWizardAnswer(step, text);
if (!answer) return {
text: ["I could not match that answer.", renderWizardStep(step)].join("\n"),
configWritten: false
};
const validationError = await bridge.session.answer(step.id, answer.value);
return validationError ? {
text: [validationError, renderWizardStep(step)].join("\n\n"),
configWritten: false
} : await this.pump();
}
async startChannel(channel) {
const run = this.options.dependencies?.runChannelSetupWizard;
return await this.start({
kind: "channel",
label: channel,
autoSelectChannel: channel,
run: async (prompter) => run ? await run(channel, prompter, this.options.beforePersistentApply) : await (await loadHostedRuntime()).runHostedChannelSetup(channel, prompter, this.options.beforePersistentApply)
});
}
async startSkills() {
const run = this.options.dependencies?.runSkillsSetupWizard;
return await this.start({
kind: "skills",
label: "skills",
run: async (prompter) => run ? await run(prompter, this.options.beforePersistentApply) : await (await loadHostedRuntime()).runHostedSkillsSetup(prompter, this.options.beforePersistentApply)
});
}
async startSearch() {
const run = this.options.dependencies?.runSearchSetupWizard;
return await this.start({
kind: "search",
label: "web search",
run: async (prompter) => run ? await run(prompter, this.options.beforePersistentApply) : await (await loadHostedRuntime()).runHostedSearchSetup(prompter, this.options.beforePersistentApply)
});
}
async startGateway() {
const run = this.options.dependencies?.runGatewaySetupWizard;
const result = await this.start({
kind: "gateway",
label: "gateway",
run: async (prompter) => run ? await run(prompter, this.options.beforePersistentApply) : await (await loadHostedRuntime()).runHostedGatewaySetup(prompter, this.options.beforePersistentApply)
});
if (this.options.surface !== "gateway" || !this.bridge) return result;
const warning = ["Before we start: changing the Gateway port, bind address, or auth credential requires a Gateway restart to apply.", "That restart may disconnect this chat, and you may need to sign in to the Control UI again with the new address or credential."].join(" ");
return {
...result,
text: [warning, result.text].filter(Boolean).join("\n\n")
};
}
async startMemoryImport() {
const run = this.options.dependencies?.runMemoryImportWizard;
const providers = [];
return await this.start({
kind: "memory-import",
label: "memory import",
memoryImportProviders: providers,
run: async (prompter) => run ? await run(prompter, this.options.beforePersistentApply, (value) => providers.push(value)) : await (await loadHostedRuntime()).runHostedMemoryImport(prompter, this.options.beforePersistentApply, (value) => providers.push(value))
});
}
async start(params) {
const completion = {
status: "applied",
...params.memoryImportProviders ? { memoryImportProviders: params.memoryImportProviders } : {}
};
const session = new WizardSession(async (prompter) => {
const result = await params.run(prompter);
if (typeof result === "string") completion.status = result;
else if (result) completion.memoryImport = result;
});
this.bridge = {
session,
step: null,
kind: params.kind,
label: params.label,
completion,
...params.autoSelectChannel ? { autoSelectChannel: params.autoSelectChannel } : {}
};
return await this.pump();
}
tryAutoSelect(step) {
const bridge = this.bridge;
const channel = bridge?.autoSelectChannel;
if (!bridge || !channel || step.type !== "select" && step.type !== "multiselect") return null;
const match = (step.options ?? []).find((option) => typeof option.value === "string" && option.value.toLowerCase() === channel);
if (!match) return null;
bridge.autoSelectChannel = void 0;
return { value: step.type === "multiselect" ? [match.value] : match.value };
}
async pump() {
const bridge = this.bridge;
if (!bridge) return {
text: "",
configWritten: false
};
const result = await bridge.session.next();
if (result.done) {
this.bridge = null;
const label = bridge.label;
if (result.status === "done") {
if (bridge.kind === "memory-import") try {
return {
text: await (await loadHostedRuntime()).renderMemoryImport(bridge.completion.memoryImport, this.options.dependencies?.appendAuditEntry),
configWritten: false
};
} catch (error) {
log.warn(`memory import completed without audit entry: ${formatErrorMessage(error)}`);
return {
text: await (await loadHostedRuntime()).renderMemoryImport(bridge.completion.memoryImport, async () => ""),
configWritten: false
};
}
if (bridge.completion.status === "kept-current") return {
text: `${label[0]?.toUpperCase() ?? "S"}${label.slice(1)} setup kept the current configuration. Nothing was changed.`,
configWritten: false
};
await this.auditSetup(bridge);
return {
text: (bridge.kind === "channel" ? [`Done — ${label} is configured.`, "Say `restart gateway` to apply channel changes, or `channels` to review."] : bridge.kind === "skills" ? ["Done — skills dependency setup is complete."] : bridge.kind === "search" ? ["Done — web search setup is complete.", "Restart the Gateway if the selected provider or plugin changed."] : ["Done — gateway settings saved.", "Restart the Gateway to apply them (`restart gateway`)."]).join("\n"),
configWritten: true
};
}
if (bridge.kind === "memory-import") try {
await (await loadHostedRuntime()).auditMemoryImport(bridge.completion.memoryImportProviders ?? [], this.options.dependencies?.appendAuditEntry);
} catch (error) {
log.warn(`memory import completed without audit entry: ${formatErrorMessage(error)}`);
}
if (result.status === "cancelled") return {
text: `${label[0]?.toUpperCase() ?? "S"}${label.slice(1)} setup cancelled. Nothing was changed beyond completed steps.`,
configWritten: false
};
return {
text: `${label[0]?.toUpperCase() ?? "S"}${label.slice(1)} setup stopped: ${result.error ?? "unknown error"}`,
configWritten: false
};
}
bridge.step = result.step ?? null;
if (bridge.step) {
const auto = this.tryAutoSelect(bridge.step);
if (auto) {
const step = bridge.step;
bridge.step = null;
await bridge.session.answer(step.id, auto.value);
return await this.pump();
}
if (this.options.surface === "cli" && bridge.step.sensitive === true) {
bridge.session.cancel();
this.bridge = null;
return {
text: ["Sensitive input is not accepted in the OpenClaw chat because terminal input is visible.", bridge.kind === "channel" ? `Say \`open channel wizard\` and I'll hand you to the masked terminal wizard for ${bridge.label}, or run \`openclaw channels add --channel ${bridge.label}\` yourself later.` : bridge.kind === "gateway" ? "Say `open gateway wizard` and I'll hand you to the masked terminal wizard, or run `openclaw configure --section gateway` yourself later." : "Say `open search wizard` and I'll hand you to the masked terminal wizard, or run `openclaw configure --section web` yourself later."].join("\n"),
configWritten: false,
...bridge.kind === "channel" ? { sensitiveChannel: bridge.label } : {}
};
}
if (bridge.step.type === "note" || bridge.step.type === "progress") {
const step = bridge.step;
bridge.step = null;
await bridge.session.answer(step.id, void 0);
const next = await this.pump();
return {
...next,
text: [renderWizardStep(step), next.text].filter(Boolean).join("\n\n")
};
}
if (bridge.step.type === "action" && bridge.step.executor !== "client") {
const step = bridge.step;
bridge.step = null;
await bridge.session.answer(step.id, true);
return await this.pump();
}
}
return {
text: bridge.step ? renderWizardStep(bridge.step) : "",
configWritten: false
};
}
async auditSetup(bridge) {
const entry = bridge.kind === "channel" ? {
operation: "channels.setup",
summary: `Configured channel ${bridge.label} via chat setup`,
details: { channel: bridge.label }
} : bridge.kind === "skills" ? {
operation: "skills.setup",
summary: "Completed skills dependency setup via chat",
details: { capability: "skills" }
} : bridge.kind === "search" ? {
operation: "search.setup",
summary: "Configured web search via chat setup",
details: { capability: "web-search" }
} : {
operation: "gateway.setup",
summary: "Configured Gateway via chat setup",
details: { capability: "gateway" }
};
try {
await (this.options.dependencies?.appendAuditEntry ?? (await import("./system-agent/audit.js")).appendSystemAgentAuditEntry)(entry);
} catch (error) {
log.warn(`${bridge.kind} setup completed without audit entry: ${formatErrorMessage(error)}`);
}
}
};
//#endregion
//#region src/system-agent/post-write-verification.ts
function unavailable(reason) {
return [`⚠ The write was applied, but post-write verification is unavailable: ${reason}.`, "Run `openclaw doctor --fix` on the machine running OpenClaw, then verify the configuration before continuing."].join("\n");
}
async function verifyConfigAfterSystemAgentWrite(resolveRepair) {
let issuesText;
try {
const { readConfigFileSnapshot } = await import("./config/config.js");
const snapshot = await readConfigFileSnapshot();
if (!snapshot.exists) return unavailable("openclaw.json was not found");
if (snapshot.valid) return null;
const issues = (snapshot.issues ?? []).map((issue) => `${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
issuesText = issues.length > 0 ? issues.join("\n") : "unknown validation failure";
} catch {
return unavailable("openclaw.json could not be read");
}
const notice = `⚠ openclaw.json failed validation after that write:\n${issuesText}`;
let recovery;
try {
recovery = await resolveRepair(`[config-verify] The config file is now invalid:\n${issuesText}\nPropose one corrective command from the allowed list.`);
} catch (error) {
if (!isSystemAgentInferenceUnavailableError(error)) throw error;
return `${notice}\nThe write was applied, but inference could not propose a repair. Run \`openclaw doctor --fix\` on the machine running OpenClaw, then try again.`;
}
return recovery.text ? `${notice}\n\n${recovery.text}` : `${notice}\nUse \`config schema <path>\` here to check the expected shape. Or, with OpenClaw stopped, run \`openclaw doctor --fix\` on the machine running it.`;
}
//#endregion
//#region src/system-agent/chat-engine.ts
/**
* One conversation with OpenClaw, independent of transport. The facade owns
* serialization, history, and the verified inference session; concept owners
* route turns and host setup wizards behind the stable public entrypoint.
*/
var SystemAgentChatEngine = class {
constructor(options, internals = {}) {
this.options = options;
this.history = [];
this.turnQueue = Promise.resolve();
const binding = options?.verifiedInference;
if (!binding) throw new SystemAgentInferenceUnavailableError("conversation");
this.verifiedInference = binding;
this.agentSession = createSystemAgentSession(binding);
this.wizard = new ChatWizardHost({
surface: options.surface,
beforePersistentApply: async (runtime) => {
await this.requirePersistentApplyInference(runtime);
},
dependencies: internals.wizardDependencies
});
this.router = new ChatTurnRouter(options, { executeOperation: internals.executeOperation }, this.agentSession, this.wizard, {
requireVerifiedInference: async () => await this.requireVerifiedInference(),
requirePersistentApplyInference: async (runtime) => await this.requirePersistentApplyInference(runtime),
rebindVerifiedInference: (next) => this.rebindVerifiedInference(next),
getVerifiedInference: () => this.verifiedInference,
loadOverview: async () => await this.loadOverview(),
getHistory: () => this.history,
verifyConfigAfterWrite: async () => await this.verifyConfigAfterWrite()
});
}
propose(operation) {
return this.router.propose(operation);
}
getPendingOperatorProposal() {
return this.router.getPendingOperatorProposal();
}
async resolveOperatorApproval(decision, proposalHash, beforePersistentApply, terminalStatus) {
const turn = this.turnQueue.then(async () => {
const reply = await this.router.resolveOperatorApproval(decision, proposalHash, beforePersistentApply);
if (reply && terminalStatus && !reply.applied) reply.text = `OpenClaw change ${terminalStatus}. No change. Retry the request if it is still needed.`;
if (reply && decision === "allow-once" && !reply.applied) reply.text += " Check the current settings and OpenClaw status before retrying.";
if (reply?.text) this.history.push({
role: "assistant",
text: reply.text
});
return reply;
});
this.turnQueue = turn.catch(() => void 0);
return await turn;
}
noteAssistantMessage(text) {
this.history.push({
role: "assistant",
text
});
}
seedHistory(turns) {
this.history.push(...turns.map((turn) => ({
...turn,
text: turn.role === "user" ? redactSensitiveCommandText(turn.text) : turn.text
})));
}
historyLength() {
return this.history.length;
}
historySince(index) {
return this.history.slice(index).map((turn) => ({
role: turn.role,
text: turn.text
}));
}
async dispose() {
this.wizard.dispose();
await cleanupSystemAgentSession(this.agentSession);
}
/**
* Project the live hosted-wizard interaction onto a rejoin reply so a
* reconnecting client re-renders the answer controls this session still
* awaits; a no-op when no wizard is active.
*/
decorateRejoinReply(reply) {
return this.wizard.decorateReply(reply);
}
async handle(text, options) {
const turn = this.turnQueue.then(async () => {
await this.requireVerifiedInference();
const sensitiveTurn = this.wizard.sensitiveInputPending;
const reply = await this.router.resolveTurn(text, options);
return this.completeTurn(reply, sensitiveTurn ? "<redacted secret>" : redactSensitiveCommandText(text));
});
this.turnQueue = turn.catch(() => void 0);
return await turn;
}
async answerWizard(answer) {
const turn = this.turnQueue.then(async () => {
await this.requireVerifiedInference();
const result = await this.router.answerWizard(this.wizard.answer(answer));
return this.completeTurn({
text: result.text,
action: "none"
}, result.userHistoryText);
});
this.turnQueue = turn.catch(() => void 0);
return await turn;
}
async cancelWizard(cancel) {
const turn = this.turnQueue.then(async () => {
const result = await this.router.answerWizard(this.wizard.cancel(cancel));
return this.completeTurn({
text: result.text,
action: "none"
}, result.userHistoryText);
});
this.turnQueue = turn.catch(() => void 0);
return await turn;
}
completeTurn(reply, userHistoryText) {
const completed = this.wizard.decorateReply(reply);
this.history.push({
role: "user",
text: userHistoryText
});
if (completed.text) this.history.push({
role: "assistant",
text: completed.text
});
return completed;
}
async loadOverview() {
const route = await this.requireVerifiedInference();
return {
...await (this.options.deps?.loadOverview ?? loadSystemAgentOverview)({ agentId: route.agentId }),
defaultModel: route.modelLabel
};
}
async planGreeting(params) {
const planner = this.options.planGreeting;
const plan = planner ? await planner(params) : await import("./assistant-CadvMdsB.js").then(({ planSystemAgentGreetingWithConfiguredModel }) => planSystemAgentGreetingWithConfiguredModel({
...params,
verifiedInference: this.verifiedInference,
deps: this.options.deps
}));
if (plan) await this.requireVerifiedInference();
return plan;
}
async requireVerifiedInference() {
const binding = this.verifiedInference;
if (this.agentSession.verifiedInference !== binding) return this.throwInferenceUnavailable();
try {
const route = await resolveSystemAgentVerifiedInferenceRoute(binding, this.options.deps);
if (route) return route;
} catch (error) {
return this.throwInferenceUnavailable([error]);
}
return this.throwInferenceUnavailable();
}
async requirePersistentApplyInference(runtime) {
const binding = this.verifiedInference;
if (this.agentSession.verifiedInference !== binding) return this.throwInferenceUnavailable();
try {
const { resolvePersistentApplyInference } = await import("./system-agent/setup-inference.js");
const route = await resolvePersistentApplyInference({
binding,
runtime,
deps: this.options.deps
});
if (route) return route;
} catch (error) {
if (isSystemAgentInferenceUnavailableError(error)) return this.throwInferenceUnavailable(error.failures, false);
return this.throwInferenceUnavailable([error], false);
}
return this.throwInferenceUnavailable([], false);
}
rebindVerifiedInference(binding) {
if (binding.execution.agentId !== this.verifiedInference.execution.agentId) return;
delete this.agentSession.cliSession;
this.verifiedInference = binding;
this.agentSession.verifiedInference = binding;
}
throwInferenceUnavailable(failures = [], cancelWizard = true) {
this.router.clearForInferenceLoss();
delete this.agentSession.cliSession;
if (cancelWizard) this.wizard.dispose();
this.history.splice(0);
throw new SystemAgentInferenceUnavailableError("conversation", failures);
}
async verifyConfigAfterWrite() {
return await verifyConfigAfterSystemAgentWrite((message) => this.router.resolveAssistantTurn(message, false));
}
};
//#endregion
//#region src/system-agent/onboarding-welcome.ts
/**
* Card-client questions for the two welcome variants. Replies are texts the
* engine already understands; the prose welcome always stands alone for
* text-only clients (macOS app, TUI).
*/
const READY_WELCOME_QUESTION = {
id: "onboarding-next-step",
header: "Next step",
question: "What would you like to do first?",
options: [
{
label: "Talk to my agent",
reply: "talk to agent",
recommended: true,
description: "Meet your agent right here."
},
{
label: "Connect WhatsApp",
reply: "connect whatsapp"
},
{
label: "Connect Telegram",
reply: "connect telegram"
},
{
label: "See all channels",
reply: "channels"
}
],
isOther: true,
skipAction: "exit"
};
const SETUP_WELCOME_QUESTION = {
id: "onboarding-apply-setup",
header: "Ready when you are",
question: "Should I set all of that up now?",
options: [{
label: "Yes — set it up",
reply: "yes",
recommended: true
}, {
label: "What will you change?",
reply: "what exactly will you set up?",
description: "Ask before anything is written."
}],
isOther: true
};
/**
* The basic bootstrap is conversational: the welcome message carries the plan
* and the engine holds it as the pending proposal, so a bare "yes" applies it.
* This path starts only after a live inference turn. Already-configured
* installs get the channels/handoff guide instead.
*/
/**
* "Configured" must match the app onboarding gate (wizard metadata or gateway
* auth), not just a model: a model-only config would otherwise get the
* ready-guide welcome while the gate stays locked, stranding the page.
*/
async function loadAuthoredSetupConfig(params) {
const authoredConfig = await (async () => {
if (!params.configExists || !params.configValid) return;
try {
const { readConfigFileSnapshot } = await import("./config/config.js");
const snapshot = await readConfigFileSnapshot();
return snapshot.sourceConfig ?? snapshot.config ?? {};
} catch {
return;
}
})();
const auth = authoredConfig?.gateway?.auth;
const hasAuthMode = normalizeSecretInputString(auth?.mode) !== void 0;
const hasAuthSecret = isSecretRef(auth?.token) || normalizeSecretInputString(auth?.token) !== void 0 || isSecretRef(auth?.password) || normalizeSecretInputString(auth?.password) !== void 0;
const hasAuthoredSetup = authoredConfig?.wizard !== void 0 && Object.keys(authoredConfig.wizard).length > 0 || hasAuthMode || hasAuthSecret;
return {
...authoredConfig ? { authoredConfig } : {},
hasAuthoredSetup
};
}
async function buildOnboardingWelcome(params) {
const overview = await params.engine.loadOverview();
const { authoredConfig, hasAuthoredSetup } = await loadAuthoredSetupConfig({
configExists: overview.config.exists,
configValid: overview.config.valid
});
const localSetup = params.localRecovery === true && overview.config.exists && overview.config.valid && authoredConfig !== void 0 && authoredConfig?.gateway?.mode !== "remote" ? (await import("./local-onboarding-state-COhpjJFR.js")).readLocalOnboardingStateForConfig(overview.config.path, authoredConfig) : void 0;
const pendingSetup = localSetup?.status === "pending" ? localSetup : void 0;
const defaultModel = overview.defaultModel?.trim();
const requestedWorkspace = params.workspace?.trim() ? resolveUserPath(params.workspace.trim()) : void 0;
const authoredWorkspace = authoredConfig?.agents?.defaults?.workspace?.trim() ? resolveUserPath(authoredConfig.agents.defaults.workspace.trim()) : void 0;
if (hasAuthoredSetup && !pendingSetup && defaultModel && (!requestedWorkspace || requestedWorkspace === authoredWorkspace)) {
const welcome = formatSystemAgentOnboardingWelcome(overview);
params.engine.noteAssistantMessage(welcome);
return {
text: welcome,
question: READY_WELCOME_QUESTION
};
}
if (!defaultModel) throw new Error("OpenClaw onboarding requires working inference first. Run `openclaw onboard` on the machine running OpenClaw to configure and verify a default model.");
const { DEFAULT_WORKSPACE } = await import("./onboard-helpers-DC4-A9ie.js");
const workspace = resolveUserPath(pendingSetup?.workspace || requestedWorkspace || authoredWorkspace || DEFAULT_WORKSPACE);
params.engine.propose({
kind: "setup",
workspace,
...params.agentName ? { agentName: params.agentName } : {}
});
const welcome = [
"## Hi, I'm OpenClaw — let's hatch your agent.",
"",
"No menus here: tell me what you want and I'll do the configuring. I looked around this machine:",
"",
`- AI: ${defaultModel} — already verified with a real reply; switching later is one sentence.`,
`- Workspace: ${shortenHomePath(workspace)}`,
"- Gateway: runs locally, private to this machine (token auth).",
"",
"Say **yes** and I'll set all of that up now.",
"",
"Heads up: your agent gets real access to this machine — https://docs.openclaw.ai/security",
"Afterwards: `connect discord`, `connect slack`, `connect telegram`, `connect whatsapp` (or `channels` for the full list), then `talk to agent` to meet your agent."
].join("\n");
params.engine.noteAssistantMessage(welcome);
return {
text: welcome,
question: SETUP_WELCOME_QUESTION
};
}
//#endregion
export { SystemAgentChatEngine as n, SystemAgentWizardAnswerError as r, buildOnboardingWelcome as t };