openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
359 lines (358 loc) • 18 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { i as isCronSessionKey } from "./session-key-utils-Bx3apsJ3.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { o as callGateway } from "./call-B5-GYOlf.js";
import { o as normalizeDeliveryContext } from "./delivery-context.shared-CpAdEETO.js";
import { t as INTERNAL_MESSAGE_CHANNEL } from "./message-channel-constants-CgI32lqD.js";
import "./message-channel-BiOeMu0l.js";
import { C as waitForEmbeddedAgentRunEnd, s as isEmbeddedAgentRunActive } from "./runs-B4dP_Q30.js";
import { c as stripLeadingSilentToken, l as stripSilentToken, n as SILENT_REPLY_TOKEN, o as isSilentReplyText, s as startsWithSilentToken } from "./tokens-U6o7_k27.js";
import { s as getSubagentDepthFromSessionStore } from "./subagent-capabilities-Cva3gXJL.js";
import "./delivery-context-DNxwKU0n.js";
import { a as resolveSubagentAnnounceTimeoutMs, g as formatAgentInternalEventsForPrompt, i as loadSessionEntryByKey, o as resolveSubagentCompletionOrigin, r as loadRequesterSessionEntry, s as runAnnounceDeliveryWithRetry, t as deliverSubagentAnnouncement } from "./subagent-announce-delivery-B2BgoPnM.js";
import { r as dispatchGatewayMethodInProcess } from "./server-plugins-DUe67FFK.js";
import { a as captureSubagentCompletionReply, c as readLatestSubagentOutputWithRetry, i as buildCompactAnnounceStatsLine, l as readSubagentOutput, m as isAnnounceSkip, n as applySubagentWaitOutcome, o as dedupeLatestChildCompletionRows, r as buildChildCompletionFindings, s as filterCurrentDirectChildCompletionRows, t as deleteSubagentSessionForCleanup, u as waitForSubagentRunOutcome } from "./subagent-session-cleanup-BHgCTKjZ.js";
import { n as buildAnnounceIdempotencyKey, t as buildAnnounceIdFromChildRun } from "./announce-idempotency-DRIcQ039.js";
import { t as buildSubagentSystemPrompt } from "./subagent-system-prompt-BqWslgg9.js";
import { t as resolveAnnounceOrigin } from "./subagent-announce-origin-CzTepT3F.js";
//#region src/agents/subagent-announce.ts
/**
* Subagent completion announcement coordinator.
*
* Captures child output, applies wait outcomes, routes announcements, and performs cleanup decisions.
*/
const defaultSubagentAnnounceDeps = {
callGateway,
dispatchGatewayMethodInProcess,
getRuntimeConfig,
loadSubagentRegistryRuntime
};
let subagentAnnounceDeps = defaultSubagentAnnounceDeps;
const subagentRegistryRuntimeLoader = createLazyImportLoader(() => import("./subagent-announce.registry.runtime.js"));
function loadSubagentRegistryRuntime() {
return subagentRegistryRuntimeLoader.load();
}
function buildAnnounceReplyInstruction(params) {
if (params.requesterIsSubagent) return `Convert this completion into a concise internal orchestration update for your parent agent in your own words. Keep this internal context private (don't mention system/log/stats/session details or announce type). If this result is duplicate or no update is needed, reply ONLY: ${SILENT_REPLY_TOKEN}.`;
if (params.expectsCompletionMessage) return `A completed ${params.announceType} is ready for parent review. Review/verify the result above before deciding whether the original task is done. If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update. Keep this internal context private (don't mention system/log/stats/session details or announce type). Reply ONLY: ${SILENT_REPLY_TOKEN} when no user-facing update is needed.`;
return `A completed ${params.announceType} is ready for parent review. Review/verify the result above before deciding whether the original task is done. If additional action is required, continue the task or record a follow-up; otherwise send a truthful user-facing update. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the internal event text verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if this exact result was already delivered to the user in this same turn.`;
}
function buildAnnounceSteerMessage(events) {
return formatAgentInternalEventsForPrompt(events) || "A background task finished. Process the completion update now.";
}
function hasUsableSessionEntry(entry) {
if (!entry || typeof entry !== "object") return false;
const sessionId = entry.sessionId;
return typeof sessionId !== "string" || sessionId.trim() !== "";
}
function buildDescendantWakeMessage(params) {
return [
"[Subagent Context] Your prior run ended while waiting for descendant subagent completions.",
"[Subagent Context] All pending descendants for that run have now settled.",
"[Subagent Context] Continue your workflow using these results. Spawn more subagents if needed, otherwise send your final answer.",
"",
`Task: ${params.taskLabel}`,
"",
params.findings
].join("\n");
}
const WAKE_RUN_SUFFIX = ":wake";
function stripWakeRunSuffixes(runId) {
let next = runId.trim();
while (next.endsWith(WAKE_RUN_SUFFIX)) next = next.slice(0, -5);
return next || runId.trim();
}
function isWakeContinuationRun(runId) {
const trimmed = runId.trim();
if (!trimmed) return false;
return stripWakeRunSuffixes(trimmed) !== trimmed;
}
function stripAndClassifyReply(text) {
let result = text;
let didStrip = false;
const hasLeadingSilentToken = startsWithSilentToken(result, SILENT_REPLY_TOKEN);
if (hasLeadingSilentToken) {
result = stripLeadingSilentToken(result, SILENT_REPLY_TOKEN);
didStrip = true;
}
if (hasLeadingSilentToken || result.toLowerCase().includes("NO_REPLY".toLowerCase())) {
result = stripSilentToken(result, SILENT_REPLY_TOKEN);
didStrip = true;
}
if (didStrip && (!result.trim() || isSilentReplyText(result, "NO_REPLY") || isAnnounceSkip(result))) return null;
return result;
}
async function wakeSubagentRunAfterDescendants(params) {
if (params.signal?.aborted) return false;
if (!hasUsableSessionEntry(loadSessionEntryByKey(params.childSessionKey))) return false;
const announceTimeoutMs = resolveSubagentAnnounceTimeoutMs(subagentAnnounceDeps.getRuntimeConfig());
const wakeMessage = buildDescendantWakeMessage({
findings: params.findings,
taskLabel: params.taskLabel
});
let wakeRunId;
try {
wakeRunId = normalizeOptionalString((await runAnnounceDeliveryWithRetry({
operation: "descendant wake agent call",
signal: params.signal,
run: async () => await subagentAnnounceDeps.dispatchGatewayMethodInProcess("agent", {
sessionKey: params.childSessionKey,
message: wakeMessage,
deliver: false,
inputProvenance: {
kind: "inter_session",
sourceSessionKey: params.childSessionKey,
sourceChannel: "webchat",
sourceTool: "subagent_announce"
},
idempotencyKey: buildAnnounceIdempotencyKey(`${params.announceId}:wake`)
}, { timeoutMs: announceTimeoutMs })
}))?.runId) ?? "";
} catch {
return false;
}
if (!wakeRunId) return false;
const { replaceSubagentRunAfterSteer } = await loadSubagentRegistryRuntime();
return replaceSubagentRunAfterSteer({
previousRunId: params.runId,
nextRunId: wakeRunId,
preserveFrozenResultFallback: true
});
}
async function runSubagentAnnounceFlow(params) {
let didAnnounce = false;
const expectsCompletionMessage = params.expectsCompletionMessage === true;
const announceType = params.announceType ?? "subagent task";
let shouldDeleteChildSession = params.cleanup === "delete";
try {
let targetRequesterSessionKey = params.requesterSessionKey;
let targetRequesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
const childSessionId = (() => {
const entry = loadSessionEntryByKey(params.childSessionKey);
return typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId.trim() : void 0;
})();
const settleTimeoutMs = Math.min(Math.max(params.timeoutMs, 1), 12e4);
let reply = params.roundOneReply;
let outcome = params.outcome;
if (childSessionId && isEmbeddedAgentRunActive(childSessionId)) {
if (!await waitForEmbeddedAgentRunEnd(childSessionId, settleTimeoutMs) && isEmbeddedAgentRunActive(childSessionId)) {
shouldDeleteChildSession = false;
if (outcome?.status !== "timeout" || params.cleanup === "delete") return false;
}
}
if (!reply && params.waitForCompletion !== false) {
const applied = applySubagentWaitOutcome({
wait: await waitForSubagentRunOutcome(params.childRunId, settleTimeoutMs),
outcome,
startedAt: params.startedAt,
endedAt: params.endedAt
});
outcome = applied.outcome;
params.startedAt = applied.startedAt;
params.endedAt = applied.endedAt;
}
if (!outcome) outcome = { status: "unknown" };
const failedTerminalOutcome = outcome.status === "error";
const allowFailedOutputCapture = !failedTerminalOutcome || !params.roundOneReply && !params.fallbackReply;
if (failedTerminalOutcome) reply = void 0;
let requesterDepth = getSubagentDepthFromSessionStore(targetRequesterSessionKey);
const requesterIsInternalSession = () => requesterDepth >= 1 || isCronSessionKey(targetRequesterSessionKey);
let childCompletionFindings;
let subagentRegistryRuntime;
try {
subagentRegistryRuntime = await subagentAnnounceDeps.loadSubagentRegistryRuntime();
if (requesterDepth >= 1 && subagentRegistryRuntime.shouldIgnorePostCompletionAnnounceForSession(targetRequesterSessionKey)) return true;
if (Math.max(0, subagentRegistryRuntime.countPendingDescendantRuns(params.childSessionKey)) > 0 && announceType !== "cron job") {
shouldDeleteChildSession = false;
return false;
}
if (typeof subagentRegistryRuntime.listSubagentRunsForRequester === "function") {
const directChildren = subagentRegistryRuntime.listSubagentRunsForRequester(params.childSessionKey, { requesterRunId: params.childRunId });
if (Array.isArray(directChildren) && directChildren.length > 0) childCompletionFindings = buildChildCompletionFindings(dedupeLatestChildCompletionRows(filterCurrentDirectChildCompletionRows(directChildren, {
requesterSessionKey: params.childSessionKey,
getLatestSubagentRunByChildSessionKey: subagentRegistryRuntime.getLatestSubagentRunByChildSessionKey
})));
}
} catch {}
const announceId = buildAnnounceIdFromChildRun({
childSessionKey: params.childSessionKey,
childRunId: params.childRunId
});
const childRunAlreadyWoken = isWakeContinuationRun(params.childRunId);
if (params.wakeOnDescendantSettle === true && childCompletionFindings?.trim() && !childRunAlreadyWoken) {
const wakeAnnounceId = buildAnnounceIdFromChildRun({
childSessionKey: params.childSessionKey,
childRunId: stripWakeRunSuffixes(params.childRunId)
});
if (await wakeSubagentRunAfterDescendants({
runId: params.childRunId,
childSessionKey: params.childSessionKey,
taskLabel: params.label || params.task || "task",
findings: childCompletionFindings,
announceId: wakeAnnounceId,
signal: params.signal
})) {
shouldDeleteChildSession = false;
return true;
}
}
if (!childCompletionFindings) {
const fallbackReply = failedTerminalOutcome ? void 0 : normalizeOptionalString(params.fallbackReply);
const fallbackIsSilent = Boolean(fallbackReply) && (isAnnounceSkip(fallbackReply) || isSilentReplyText(fallbackReply, "NO_REPLY"));
if (!reply && allowFailedOutputCapture) reply = await readSubagentOutput(params.childSessionKey, outcome);
if (!reply?.trim() && allowFailedOutputCapture) reply = await readLatestSubagentOutputWithRetry({
sessionKey: params.childSessionKey,
maxWaitMs: params.timeoutMs,
outcome
});
if (!reply?.trim() && fallbackReply && !fallbackIsSilent) reply = fallbackReply;
if (outcome?.status === "timeout" && reply?.trim() && params.waitForCompletion !== false) try {
const applied = applySubagentWaitOutcome({
wait: await waitForSubagentRunOutcome(params.childRunId, 0),
outcome,
startedAt: params.startedAt,
endedAt: params.endedAt
});
outcome = applied.outcome;
params.startedAt = applied.startedAt;
params.endedAt = applied.endedAt;
} catch {}
if (isAnnounceSkip(reply) || isSilentReplyText(reply, "NO_REPLY")) if (fallbackReply && !fallbackIsSilent) {
const cleaned = stripAndClassifyReply(fallbackReply);
if (cleaned === null) return true;
reply = cleaned;
} else return true;
else if (reply) {
const cleaned = stripAndClassifyReply(reply);
if (cleaned === null) if (fallbackReply && !fallbackIsSilent) {
const cleanedFallback = stripAndClassifyReply(fallbackReply);
if (cleanedFallback === null) return true;
reply = cleanedFallback;
} else return true;
else reply = cleaned;
}
}
if (!outcome) outcome = { status: "unknown" };
const statusLabel = outcome.status === "ok" ? "completed; ready for parent review" : outcome.status === "timeout" ? "timed out" : outcome.status === "error" ? `failed: ${outcome.error || "unknown error"}` : "finished with unknown status";
const taskLabel = params.label || params.task || "task";
const announceSessionId = childSessionId || "unknown";
const findings = childCompletionFindings || reply || "(no output)";
let requesterIsSubagent = requesterIsInternalSession();
if (requesterIsSubagent) {
const { isSubagentSessionRunActive, resolveRequesterForChildSession, shouldIgnorePostCompletionAnnounceForSession } = subagentRegistryRuntime ?? await loadSubagentRegistryRuntime();
if (!isSubagentSessionRunActive(targetRequesterSessionKey)) {
if (shouldIgnorePostCompletionAnnounceForSession(targetRequesterSessionKey)) return true;
if (!hasUsableSessionEntry(loadSessionEntryByKey(targetRequesterSessionKey))) {
const fallback = resolveRequesterForChildSession(targetRequesterSessionKey);
if (!fallback?.requesterSessionKey) {
shouldDeleteChildSession = false;
return false;
}
targetRequesterSessionKey = fallback.requesterSessionKey;
targetRequesterOrigin = normalizeDeliveryContext(fallback.requesterOrigin) ?? targetRequesterOrigin;
requesterDepth = getSubagentDepthFromSessionStore(targetRequesterSessionKey);
requesterIsSubagent = requesterIsInternalSession();
}
}
}
const replyInstruction = buildAnnounceReplyInstruction({
requesterIsSubagent,
announceType,
expectsCompletionMessage
});
const statsLine = await buildCompactAnnounceStatsLine({
sessionKey: params.childSessionKey,
startedAt: params.startedAt,
endedAt: params.endedAt
});
const internalEvents = [{
type: "task_completion",
source: announceType === "cron job" ? "cron" : "subagent",
childSessionKey: params.childSessionKey,
childSessionId: announceSessionId,
announceType,
taskLabel,
status: outcome.status,
statusLabel,
result: findings,
statsLine,
replyInstruction
}];
const triggerMessage = buildAnnounceSteerMessage(internalEvents);
let directOrigin = targetRequesterOrigin;
if (!requesterIsSubagent) {
const { entry } = loadRequesterSessionEntry(targetRequesterSessionKey);
directOrigin = resolveAnnounceOrigin(entry, targetRequesterOrigin);
}
const completionDirectOrigin = expectsCompletionMessage && !requesterIsSubagent ? await resolveSubagentCompletionOrigin({
childSessionKey: params.childSessionKey,
requesterSessionKey: targetRequesterSessionKey,
requesterOrigin: directOrigin,
childRunId: params.childRunId,
spawnMode: params.spawnMode,
expectsCompletionMessage
}) : targetRequesterOrigin;
const directIdempotencyKey = buildAnnounceIdempotencyKey(announceId);
const delivery = await deliverSubagentAnnouncement({
requesterSessionKey: targetRequesterSessionKey,
announceId,
triggerMessage,
steerMessage: triggerMessage,
internalEvents,
summaryLine: taskLabel,
requesterSessionOrigin: targetRequesterOrigin,
requesterOrigin: expectsCompletionMessage && !requesterIsSubagent ? completionDirectOrigin : targetRequesterOrigin,
completionDirectOrigin,
directOrigin,
sourceSessionKey: params.childSessionKey,
sourceChannel: INTERNAL_MESSAGE_CHANNEL,
sourceTool: "subagent_announce",
targetRequesterSessionKey,
requesterIsSubagent,
expectsCompletionMessage,
bestEffortDeliver: params.bestEffortDeliver,
directIdempotencyKey,
signal: params.signal
});
params.onDeliveryResult?.(delivery);
didAnnounce = delivery.delivered;
if (!delivery.delivered && delivery.path === "direct" && delivery.error) defaultRuntime.log(`[warn] Subagent completion direct announce failed for run ${params.childRunId}: ${delivery.error}`);
} catch (err) {
defaultRuntime.error?.(`Subagent announce failed: ${String(err)}`);
} finally {
if (params.label) try {
await subagentAnnounceDeps.callGateway({
method: "sessions.patch",
params: {
key: params.childSessionKey,
label: params.label
},
timeoutMs: 1e4
});
} catch {}
if (shouldDeleteChildSession) await deleteSubagentSessionForCleanup({
callGateway: subagentAnnounceDeps.callGateway,
childSessionKey: params.childSessionKey,
spawnMode: params.spawnMode
});
}
return didAnnounce;
}
const testing = { setDepsForTest(overrides) {
const callGatewayOverride = overrides?.callGateway;
const dispatchGatewayMethodInProcessOverride = overrides?.dispatchGatewayMethodInProcess ?? (callGatewayOverride ? (async (method, agentParams, options) => await callGatewayOverride({
method,
params: agentParams,
expectFinal: options?.expectFinal,
timeoutMs: options?.timeoutMs
})) : void 0);
subagentAnnounceDeps = overrides ? {
...defaultSubagentAnnounceDeps,
...overrides,
...dispatchGatewayMethodInProcessOverride ? { dispatchGatewayMethodInProcess: dispatchGatewayMethodInProcessOverride } : {}
} : defaultSubagentAnnounceDeps;
} };
//#endregion
export { testing as __testing, testing, buildSubagentSystemPrompt, captureSubagentCompletionReply, runSubagentAnnounceFlow };