openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
159 lines (158 loc) • 6.08 kB
JavaScript
import { a as snapshotStructuredInput, i as isStructuredInputRecord, o as runStructuredInput, r as compileStructuredInputUrl, t as compileStructuredInputForm } from "./structured-input-BvzwNGZ1.js";
import { createHash } from "node:crypto";
//#region src/auto-reply/reply/acp-elicitation.ts
const MAX_CORRELATION_TEXT = 128;
/** Adapts ACP wire scope and codex-acp metadata into the shared structured-input compiler. */
function parseAcpElicitationRequest(request) {
const snapshot = snapshotStructuredInput(request);
if (!isStructuredInputRecord(snapshot)) return unsupported("OpenClaw declined a malformed or over-limit ACP input request.");
const correlation = readScope(snapshot);
if (typeof correlation === "string") return unsupported(correlation);
const mode = readAcpElicitationString(snapshot, "mode");
if (mode === "url") return {
correlation,
input: compileStructuredInputUrl({
url: readValue(snapshot, "url"),
elicitationId: readValue(snapshot, "elicitationId"),
message: readValue(snapshot, "message"),
fallbackMessage: "ACP provided a URL",
protocolName: "ACP"
})
};
if (mode !== "form") return unsupported(`OpenClaw does not support ACP elicitation mode ${JSON.stringify(mode ?? "unknown")}.`);
return {
correlation,
input: compileStructuredInputForm({
schema: readValue(snapshot, "requestedSchema"),
message: readAcpElicitationString(snapshot, "message"),
fallbackMessage: "ACP needs input",
options: {
protocolName: "ACP",
minimumChoiceCount: 2,
booleanLabels: ["True", "False"],
metadata: {
secretPath: [
"_meta",
"codex",
"isSecret"
],
otherAnswerPath: [
"_meta",
"codex",
"isOtherAnswer"
],
otherQuestionIdPath: [
"_meta",
"codex",
"questionId"
]
}
}
})
};
}
function readScope(request) {
const sessionId = readValue(request, "sessionId");
const requestId = readValue(request, "requestId");
const hasSession = sessionId !== void 0;
if (hasSession === (requestId !== void 0)) return "OpenClaw declined an ACP input request with an invalid or ambiguous scope.";
if (hasSession) {
const normalizedSessionId = readCorrelationText(sessionId);
if (!normalizedSessionId) return "OpenClaw declined an ACP input request with an invalid session id.";
const toolCallId = readValue(request, "toolCallId");
const normalizedToolCallId = toolCallId === void 0 || toolCallId === null ? toolCallId : readCorrelationText(toolCallId);
if (toolCallId !== void 0 && toolCallId !== null && !normalizedToolCallId) return "OpenClaw declined an ACP input request with an invalid tool-call id.";
return {
sessionId: normalizedSessionId,
...toolCallId === void 0 ? {} : { toolCallId: normalizedToolCallId ?? null }
};
}
if (requestId === null) return { requestId };
if (typeof requestId === "number") return { requestId };
const normalizedRequestId = readCorrelationText(requestId);
return normalizedRequestId ? { requestId: normalizedRequestId } : "OpenClaw declined an ACP input request with an invalid request scope.";
}
function readCorrelationText(value) {
if (typeof value !== "string" || value.length === 0 || value.length > MAX_CORRELATION_TEXT) return;
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) return;
}
return value;
}
function readValue(record, key) {
return Object.hasOwn(record, key) ? record[key] : void 0;
}
function readAcpElicitationString(record, key) {
const value = readValue(record, key);
return typeof value === "string" ? value : void 0;
}
function unsupported(message) {
return { input: {
kind: "unsupported",
message
} };
}
//#endregion
//#region src/auto-reply/reply/acp-elicitation-handler.ts
const DEFAULT_ELICITATION_TIMEOUT_MS = 9e5;
const MAX_REQUEST_ID_TEXT = 128;
function questionId(params) {
return `acp_${createHash("sha256").update(JSON.stringify(params)).digest("hex").slice(0, 24)}_${params.batch}`;
}
function cancellation(message) {
return {
action: "cancel",
_meta: { message }
};
}
function decline(message) {
return {
action: "decline",
...message ? { _meta: { message } } : {}
};
}
function isContextRequestIdValid(value) {
return value === null || typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length > 0 && value.length <= MAX_REQUEST_ID_TEXT;
}
/** Creates the turn-owned ACP form/URL bridge used by channel delivery. */
function createAcpElicitationHandler(params) {
const delivery = { onBlockReply: async (payload) => {
await params.delivery.deliver("block", payload);
} };
return async (request, context) => {
if (!isContextRequestIdValid(context.requestId) || context.signal.aborted || !params.isActive()) return cancellation("ACP input request is no longer active.");
const parsed = parseAcpElicitationRequest(request);
const result = await runStructuredInput({
input: parsed.input,
sessionKey: params.targetSessionKey,
agentId: params.agentId,
runId: params.runId,
timeoutMs: DEFAULT_ELICITATION_TIMEOUT_MS,
delivery,
signal: context.signal,
isActive: params.isActive,
questionId: (batch) => questionId({
contextRequestId: context.requestId,
correlation: parsed.correlation,
sourceSessionKey: params.sourceSessionKey,
targetSessionKey: params.targetSessionKey,
outerRequestId: params.outerRequestId,
batch
}),
promptOptions: {
unsupportedIntro: "ACP input request could not be shown:",
urlIntro: "ACP needs confirmation:"
}
});
if (result.status === "answered") return parsed.input.kind === "ready" && parsed.input.plan.kind === "url" ? { action: "accept" } : {
action: "accept",
content: result.content
};
if (result.status === "declined") return decline(result.message);
if (result.status === "unsupported") return decline(result.message);
return cancellation(result.message ?? "ACP input request was cancelled.");
};
}
//#endregion
export { createAcpElicitationHandler };