Files
openclaw/src/agents/bash-tools.exec-approval-followup.ts

128 lines
4.1 KiB
TypeScript

import { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best-effort-delivery.js";
import { sendMessage } from "../infra/outbound/message.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { isGatewayMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
import { callGatewayTool } from "./tools/gateway.js";
type ExecApprovalFollowupParams = {
approvalId: string;
sessionKey?: string;
turnSourceChannel?: string;
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
resultText: string;
};
function buildExecDeniedFollowupPrompt(resultText: string): string {
return [
"An async command did not run.",
"Do not run the command again.",
"There is no new command output.",
"Do not mention, summarize, or reuse output from any earlier run in this session.",
"",
"Exact completion details:",
resultText.trim(),
"",
"Reply to the user in a helpful way.",
"Explain that the command did not run and why.",
"Do not claim there is new command output.",
].join("\n");
}
export function buildExecApprovalFollowupPrompt(resultText: string): string {
const trimmed = resultText.trim();
if (trimmed.startsWith("Exec denied (")) {
return buildExecDeniedFollowupPrompt(trimmed);
}
return [
"An async command the user already approved has completed.",
"Do not run the command again.",
"",
"Exact completion details:",
trimmed,
"",
"Reply to the user in a helpful way.",
"If it succeeded, share the relevant output.",
"If it failed, explain what went wrong.",
].join("\n");
}
export async function sendExecApprovalFollowup(
params: ExecApprovalFollowupParams,
): Promise<boolean> {
const sessionKey = params.sessionKey?.trim();
const resultText = params.resultText.trim();
if (!resultText) {
return false;
}
const deliveryTarget = resolveExternalBestEffortDeliveryTarget({
channel: params.turnSourceChannel,
to: params.turnSourceTo,
accountId: params.turnSourceAccountId,
threadId: params.turnSourceThreadId,
});
const normalizedTurnSourceChannel = normalizeMessageChannel(params.turnSourceChannel);
const sessionOnlyOriginChannel =
normalizedTurnSourceChannel && isGatewayMessageChannel(normalizedTurnSourceChannel)
? normalizedTurnSourceChannel
: undefined;
if (deliveryTarget.deliver) {
const requesterAgentId = sessionKey ? parseAgentSessionKey(sessionKey)?.agentId : undefined;
await sendMessage({
channel: deliveryTarget.channel,
to: deliveryTarget.to ?? "",
accountId: deliveryTarget.accountId,
threadId: deliveryTarget.threadId,
content: resultText,
agentId: requesterAgentId,
idempotencyKey: `exec-approval-followup:${params.approvalId}`,
mirror: sessionKey
? {
sessionKey,
agentId: requesterAgentId,
idempotencyKey: `exec-approval-followup:${params.approvalId}`,
}
: undefined,
});
return true;
}
if (!sessionKey) {
throw new Error("Session key or deliverable origin route is required");
}
await callGatewayTool(
"agent",
{ timeoutMs: 60_000 },
{
sessionKey,
message: buildExecApprovalFollowupPrompt(resultText),
deliver: deliveryTarget.deliver,
...(deliveryTarget.deliver ? { bestEffortDeliver: true as const } : {}),
channel: deliveryTarget.deliver ? deliveryTarget.channel : sessionOnlyOriginChannel,
to: deliveryTarget.deliver
? deliveryTarget.to
: sessionOnlyOriginChannel
? params.turnSourceTo
: undefined,
accountId: deliveryTarget.deliver
? deliveryTarget.accountId
: sessionOnlyOriginChannel
? params.turnSourceAccountId
: undefined,
threadId: deliveryTarget.deliver
? deliveryTarget.threadId
: sessionOnlyOriginChannel
? params.turnSourceThreadId
: undefined,
idempotencyKey: `exec-approval-followup:${params.approvalId}`,
},
{ expectFinal: true },
);
return true;
}