openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
430 lines (429 loc) • 18.6 kB
JavaScript
import { i as asOptionalRecord } from "./record-coerce-DHZ4bFlT.js";
import { s as asFiniteNumber } from "./number-coercion-CJQ8TR--.js";
import { f as resolveAgentIdFromSessionKey } from "./session-key-B_NoIfpX.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import "./config-C9RxTsn1.js";
import { o as callGateway } from "./call-B5-GYOlf.js";
import { u as resolveStorePath } from "./paths-NEwU8m3X.js";
import { i as readSessionEntry } from "./store-load-C4uq6Lrq.js";
import "./sessions-D6zZvoxX.js";
import { n as buildAgentRunTerminalOutcomeFromWaitResult } from "./agent-run-terminal-outcome-wSgVLvyR.js";
import "./runs-B4dP_Q30.js";
import { n as SILENT_REPLY_TOKEN, o as isSilentReplyText, t as HEARTBEAT_TOKEN } from "./tokens-U6o7_k27.js";
import { d as readSessionMessagesAsync } from "./session-utils.fs-D_8-X4jl.js";
import { n as sanitizeTextContent, t as extractAssistantText } from "./chat-history-text-D43w1rZt.js";
import { n as wrapPromptDataBlock } from "./sanitize-for-prompt-ByaJGDhT.js";
import "./server-plugins-DUe67FFK.js";
//#region src/agents/tools/sessions-send-tokens.ts
/**
* sessions_send sentinel tokens.
*
* Defines non-deliverable reply markers used by sessions_send and subagent completion delivery.
*/
/** Suppresses a subagent completion announcement. */
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
/** Suppresses a direct reply delivery. */
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
const NON_DELIVERABLE_REPLY_TOKENS = [
ANNOUNCE_SKIP_TOKEN,
REPLY_SKIP_TOKEN,
SILENT_REPLY_TOKEN,
HEARTBEAT_TOKEN
];
/** Returns true when text is exactly the announce-skip sentinel. */
function isAnnounceSkip(text) {
return (text ?? "").trim() === ANNOUNCE_SKIP_TOKEN;
}
/** Returns true when text is exactly the reply-skip sentinel. */
function isReplySkip(text) {
return (text ?? "").trim() === REPLY_SKIP_TOKEN;
}
/** Returns true when text is any non-deliverable sessions reply sentinel. */
function isNonDeliverableSessionsReply(text) {
return NON_DELIVERABLE_REPLY_TOKENS.some((token) => isSilentReplyText(text, token));
}
//#endregion
//#region src/agents/subagent-announce-capture.ts
/**
* Helpers for capturing the latest subagent completion reply after a run ends.
*
* Completion output can lag behind lifecycle state, so callers can retry briefly
* before sending an empty or stale announcement.
*/
/** Reads subagent output repeatedly until non-empty text appears or the bounded wait expires. */
async function readLatestSubagentOutputWithRetryUsing(params) {
const maxWaitMs = Math.max(0, Math.min(params.maxWaitMs, 15e3));
let waitedMs = 0;
let result;
while (waitedMs < maxWaitMs) {
result = await params.readSubagentOutput(params.sessionKey, params.outcome);
if (result?.trim()) return result;
const remainingMs = maxWaitMs - waitedMs;
if (remainingMs <= 0) break;
const sleepMs = Math.min(params.retryIntervalMs, remainingMs);
await new Promise((resolve) => {
setTimeout(resolve, sleepMs);
});
waitedMs += sleepMs;
}
return result;
}
/** Captures immediate output first, then optionally waits for a delayed completion reply. */
async function captureSubagentCompletionReplyUsing(params) {
const immediate = await params.readSubagentOutput(params.sessionKey);
if (immediate?.trim()) return immediate;
if (params.waitForReply === false) return;
return await readLatestSubagentOutputWithRetryUsing({
sessionKey: params.sessionKey,
maxWaitMs: params.maxWaitMs,
retryIntervalMs: params.retryIntervalMs,
readSubagentOutput: params.readSubagentOutput
});
}
//#endregion
//#region src/agents/subagent-yield-output.ts
/**
* sessions_yield transcript detectors.
*
* Accepts provider-specific tool-call and tool-result shapes used by transcript repair and announce capture.
*/
function readToolName(value) {
const record = asOptionalRecord(value);
if (!record) return;
for (const key of [
"name",
"toolName",
"tool_name",
"functionName",
"function_name"
]) {
const candidate = record[key];
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
}
function isToolCallBlock(value) {
const record = asOptionalRecord(value);
if (!record) return false;
return record.type === "toolCall" || record.type === "tool_use" || record.type === "toolUse" || record.type === "functionCall" || record.type === "function_call";
}
/** Returns true when an assistant message requested the sessions_yield tool. */
function assistantCallsSessionsYield(message) {
const record = asOptionalRecord(message);
if (!record || record.role !== "assistant" || !Array.isArray(record.content)) return false;
return record.content.some((block) => isToolCallBlock(block) && readToolName(block) === "sessions_yield");
}
function parseJsonObject(text) {
const trimmed = text.trim();
if (!trimmed.startsWith("{")) return;
try {
return asOptionalRecord(JSON.parse(trimmed));
} catch {
return;
}
}
function readStructuredToolPayload(content) {
const record = asOptionalRecord(content);
if (record) return record;
if (typeof content === "string") return parseJsonObject(content);
if (!Array.isArray(content)) return;
for (const block of content) {
const blockRecord = asOptionalRecord(block);
if (!blockRecord) continue;
const text = blockRecord.text;
if (typeof text !== "string") continue;
const parsed = parseJsonObject(text);
if (parsed) return parsed;
}
}
/** Returns true when a tool result represents a completed sessions_yield handoff. */
function isSessionsYieldToolResult(message, previousAssistantCalledYield) {
const record = asOptionalRecord(message);
if (!record || record.role !== "toolResult" && record.role !== "tool") return false;
if (readToolName(record) === "sessions_yield") return true;
if (!previousAssistantCalledYield) return false;
if (asOptionalRecord(record.details)?.status === "yielded") return true;
return readStructuredToolPayload(record.content)?.status === "yielded";
}
//#endregion
//#region src/agents/subagent-announce-output.ts
/**
* Subagent completion output capture.
*
* Reads child session output, detects waiting states, and formats completion findings for announcements.
*/
const FAST_TEST_RETRY_INTERVAL_MS = 8;
let subagentAnnounceOutputDeps = {
callGateway,
getRuntimeConfig,
readSessionEntry,
readSessionMessagesAsync,
resolveAgentIdFromSessionKey,
resolveStorePath
};
function isFastTestMode() {
return process.env.OPENCLAW_TEST_FAST === "1";
}
function withSubagentOutcomeTiming(outcome, timing) {
const startedAt = asFiniteNumber(timing.startedAt) ?? asFiniteNumber(outcome.startedAt);
const endedAt = asFiniteNumber(timing.endedAt) ?? asFiniteNumber(outcome.endedAt);
const nextTiming = {};
if (typeof startedAt === "number") nextTiming.startedAt = startedAt;
if (typeof endedAt === "number") nextTiming.endedAt = endedAt;
if (typeof startedAt === "number" && typeof endedAt === "number") nextTiming.elapsedMs = Math.max(0, endedAt - startedAt);
return {
...outcome,
...nextTiming
};
}
function extractSubagentAssistantText(message) {
if (!message || typeof message !== "object") return "";
if (message.role !== "assistant") return "";
const content = message.content;
if (typeof content === "string") return sanitizeTextContent(content);
return extractAssistantText(message) ?? "";
}
function countAssistantToolCalls(message) {
if (!message || typeof message !== "object") return 0;
const content = message.content;
const contentToolCalls = Array.isArray(content) ? content.filter((block) => block && typeof block === "object" && (block.type === "toolCall" || block.type === "tool_use")).length : 0;
const toolCalls = message.toolCalls ?? message.tool_calls;
return contentToolCalls + (Array.isArray(toolCalls) ? toolCalls.length : 0);
}
function summarizeSubagentOutputHistory(messages) {
const snapshot = {};
let previousAssistantCalledYield = false;
for (const message of messages) {
if (!message || typeof message !== "object") continue;
if (message.role === "assistant") {
if (assistantCallsSessionsYield(message)) {
snapshot.latestAssistantText = void 0;
snapshot.latestSilentText = void 0;
snapshot.waitingForContinuation = true;
previousAssistantCalledYield = true;
continue;
}
const text = extractSubagentAssistantText(message).trim();
if (!text) {
snapshot.latestToolCallCount = (snapshot.latestToolCallCount ?? 0) + countAssistantToolCalls(message);
snapshot.waitingForContinuation = false;
previousAssistantCalledYield = false;
continue;
}
if (isAnnounceSkip(text) || isSilentReplyText(text, "NO_REPLY")) {
snapshot.latestSilentText = text;
snapshot.latestAssistantText = void 0;
snapshot.waitingForContinuation = false;
previousAssistantCalledYield = false;
continue;
}
snapshot.latestSilentText = void 0;
snapshot.latestAssistantText = text;
snapshot.waitingForContinuation = false;
previousAssistantCalledYield = false;
continue;
}
if (isSessionsYieldToolResult(message, previousAssistantCalledYield)) {
snapshot.latestAssistantText = void 0;
snapshot.latestSilentText = void 0;
snapshot.waitingForContinuation = true;
previousAssistantCalledYield = false;
continue;
}
previousAssistantCalledYield = false;
}
return snapshot;
}
function selectSubagentOutputText(snapshot) {
if (snapshot.waitingForContinuation) return;
if (snapshot.latestSilentText) return snapshot.latestSilentText;
if (snapshot.latestAssistantText) return snapshot.latestAssistantText;
if (snapshot.latestToolCallCount && snapshot.latestToolCallCount > 0) return `${snapshot.latestToolCallCount} tool call(s) made without visible output.`;
}
async function readSubagentOutput(sessionKey, _outcome, options) {
let messages;
if (options?.sessionFile) messages = await subagentAnnounceOutputDeps.readSessionMessagesAsync(sessionKey, void 0, options.sessionFile, {
mode: "recent",
maxMessages: 100,
maxBytes: 1024 * 1024
});
const history = messages === void 0 ? await subagentAnnounceOutputDeps.callGateway({
method: "chat.history",
params: {
sessionKey,
limit: 100
}
}) : void 0;
const selected = selectSubagentOutputText(summarizeSubagentOutputHistory(messages ?? (Array.isArray(history?.messages) ? history.messages : [])));
if (selected?.trim()) return selected;
}
async function readLatestSubagentOutputWithRetry(params) {
return await readLatestSubagentOutputWithRetryUsing({
sessionKey: params.sessionKey,
maxWaitMs: params.maxWaitMs,
outcome: params.outcome,
retryIntervalMs: isFastTestMode() ? FAST_TEST_RETRY_INTERVAL_MS : 100,
readSubagentOutput
});
}
async function waitForSubagentRunOutcome(runId, timeoutMs) {
const waitMs = Math.max(0, Math.floor(timeoutMs));
return await subagentAnnounceOutputDeps.callGateway({
method: "agent.wait",
params: {
runId,
timeoutMs: waitMs
},
timeoutMs: waitMs + 2e3
});
}
function applySubagentWaitOutcome(params) {
const next = {
outcome: params.outcome,
startedAt: params.startedAt,
endedAt: params.endedAt
};
if (typeof params.wait?.startedAt === "number" && typeof next.startedAt !== "number") next.startedAt = params.wait.startedAt;
if (typeof params.wait?.endedAt === "number" && typeof next.endedAt !== "number") next.endedAt = params.wait.endedAt;
const waitError = typeof params.wait?.error === "string" ? params.wait.error : void 0;
const terminalOutcome = buildAgentRunTerminalOutcomeFromWaitResult(params.wait);
let outcome = next.outcome;
if (terminalOutcome?.status === "timeout") outcome = { status: "timeout" };
else if (terminalOutcome?.reason === "aborted" || terminalOutcome?.reason === "cancelled") outcome = {
status: "error",
error: "subagent run terminated"
};
else if (terminalOutcome?.reason === "blocked" || terminalOutcome?.reason === "failed") outcome = {
status: "error",
error: terminalOutcome.error ?? waitError
};
else if (terminalOutcome?.reason === "completed") outcome = { status: "ok" };
next.outcome = outcome ? withSubagentOutcomeTiming(outcome, next) : void 0;
return next;
}
async function captureSubagentCompletionReply(sessionKey, options) {
return await captureSubagentCompletionReplyUsing({
sessionKey,
waitForReply: options?.waitForReply,
maxWaitMs: isFastTestMode() ? 50 : 1500,
retryIntervalMs: isFastTestMode() ? FAST_TEST_RETRY_INTERVAL_MS : 100,
readSubagentOutput: async (nextSessionKey) => await readSubagentOutput(nextSessionKey, options?.outcome, { sessionFile: options?.sessionFile })
});
}
function describeSubagentOutcome(outcome) {
if (!outcome) return "unknown";
if (outcome.status === "ok") return "ok";
if (outcome.status === "timeout") return "timeout";
if (outcome.status === "error") return outcome.error?.trim() ? `error: ${outcome.error.trim()}` : "error";
return "unknown";
}
function formatChildResultData(resultText) {
return wrapPromptDataBlock({
label: "Child result",
text: resultText?.trim() || "(no output)"
}) || "Child result: (no output)";
}
function selectChildCompletionResultText(child) {
return (child.completion?.resultText ?? child.delivery?.payload?.frozenResultText ?? child.completion?.fallbackResultText ?? child.delivery?.payload?.fallbackFrozenResultText ?? child.frozenResultText ?? void 0)?.trim();
}
function buildChildCompletionFindings(children) {
const sorted = [...children].toSorted((a, b) => {
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
return (typeof a.endedAt === "number" ? a.endedAt : Number.MAX_SAFE_INTEGER) - (typeof b.endedAt === "number" ? b.endedAt : Number.MAX_SAFE_INTEGER);
});
const sections = [];
for (const [index, child] of sorted.entries()) {
const resultText = selectChildCompletionResultText(child);
const outcome = describeSubagentOutcome(child.outcome);
if (child.outcome?.status === "ok" && resultText && (isAnnounceSkip(resultText) || isSilentReplyText(resultText, "NO_REPLY"))) continue;
const title = child.label?.trim() || child.task.trim() || child.childSessionKey.trim() || `child ${index + 1}`;
const displayIndex = sections.length + 1;
sections.push([
`${displayIndex}. ${title}`,
`status: ${outcome}`,
formatChildResultData(resultText)
].join("\n"));
}
if (sections.length === 0) return;
return [
"Child completion results:",
"",
...sections
].join("\n\n");
}
function dedupeLatestChildCompletionRows(children) {
const latestByChildSessionKey = /* @__PURE__ */ new Map();
for (const child of children) {
const existing = latestByChildSessionKey.get(child.childSessionKey);
if (!existing || child.createdAt > existing.createdAt) latestByChildSessionKey.set(child.childSessionKey, child);
}
return [...latestByChildSessionKey.values()];
}
function filterCurrentDirectChildCompletionRows(children, params) {
if (typeof params.getLatestSubagentRunByChildSessionKey !== "function") return children;
return children.filter((child) => {
const latest = params.getLatestSubagentRunByChildSessionKey?.(child.childSessionKey);
if (!latest) return true;
return latest.runId === child.runId && latest.requesterSessionKey === params.requesterSessionKey;
});
}
function formatDurationShort(valueMs) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return "n/a";
const totalSeconds = Math.round(valueMs / 1e3);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor(totalSeconds % 3600 / 60);
const seconds = totalSeconds % 60;
if (hours > 0) return `${hours}h${minutes}m`;
if (minutes > 0) return `${minutes}m${seconds}s`;
return `${seconds}s`;
}
function formatTokenCount(value) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "0";
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}m`;
if (value >= 1e3) {
const formattedThousands = (value / 1e3).toFixed(1);
if (Number(formattedThousands) >= 1e3) return `${(value / 1e6).toFixed(1)}m`;
return `${formattedThousands}k`;
}
return String(Math.round(value));
}
async function buildCompactAnnounceStatsLine(params) {
const cfg = subagentAnnounceOutputDeps.getRuntimeConfig();
const agentId = subagentAnnounceOutputDeps.resolveAgentIdFromSessionKey(params.sessionKey);
const storePath = subagentAnnounceOutputDeps.resolveStorePath(cfg.session?.store, { agentId });
let entry = subagentAnnounceOutputDeps.readSessionEntry(storePath, params.sessionKey);
const tokenWaitAttempts = isFastTestMode() ? 1 : 3;
for (let attempt = 0; attempt < tokenWaitAttempts; attempt += 1) {
if (typeof entry?.inputTokens === "number" || typeof entry?.outputTokens === "number" || typeof entry?.totalTokens === "number") break;
if (!isFastTestMode()) await new Promise((resolve) => {
setTimeout(resolve, 150);
});
entry = subagentAnnounceOutputDeps.readSessionEntry(storePath, params.sessionKey);
}
const input = typeof entry?.inputTokens === "number" ? entry.inputTokens : 0;
const output = typeof entry?.outputTokens === "number" ? entry.outputTokens : 0;
const ioTotal = input + output;
const promptCache = typeof entry?.totalTokens === "number" ? entry.totalTokens : void 0;
const parts = [`runtime ${formatDurationShort(typeof params.startedAt === "number" && typeof params.endedAt === "number" ? Math.max(0, params.endedAt - params.startedAt) : void 0)}`, `tokens ${formatTokenCount(ioTotal)} (in ${formatTokenCount(input)} / out ${formatTokenCount(output)})`];
if (typeof promptCache === "number" && promptCache > ioTotal) parts.push(`prompt/cache ${formatTokenCount(promptCache)}`);
return `Stats: ${parts.join(" • ")}`;
}
//#endregion
//#region src/agents/subagent-session-cleanup.ts
/** Deletes a child subagent session and optionally emits session-mode lifecycle hooks. */
async function deleteSubagentSessionForCleanup(params) {
try {
await params.callGateway({
method: "sessions.delete",
params: {
key: params.childSessionKey,
deleteTranscript: true,
emitLifecycleHooks: params.spawnMode === "session"
},
timeoutMs: 1e4
});
} catch (error) {
params.onError?.(error);
}
}
//#endregion
export { captureSubagentCompletionReply as a, readLatestSubagentOutputWithRetry as c, withSubagentOutcomeTiming as d, ANNOUNCE_SKIP_TOKEN as f, isReplySkip as g, isNonDeliverableSessionsReply as h, buildCompactAnnounceStatsLine as i, readSubagentOutput as l, isAnnounceSkip as m, applySubagentWaitOutcome as n, dedupeLatestChildCompletionRows as o, REPLY_SKIP_TOKEN as p, buildChildCompletionFindings as r, filterCurrentDirectChildCompletionRows as s, deleteSubagentSessionForCleanup as t, waitForSubagentRunOutcome as u };