docs: document embedded runner state helpers

This commit is contained in:
Peter Steinberger
2026-06-04 00:51:06 -04:00
parent 7e85ba6139
commit 5a557b5e10
6 changed files with 52 additions and 0 deletions
@@ -9,6 +9,12 @@ import {
import { resolveProviderCacheTtlEligibility } from "../../plugins/provider-runtime.js";
import { isGooglePromptCacheEligible } from "./prompt-cache-retention.js";
/**
* Cache-TTL eligibility and session markers for provider prompt-cache retention.
*
* Providers can override eligibility through plugin runtime hooks; built-in fallback logic covers
* Anthropic-family and Google prompt-cache semantics.
*/
type CustomEntryLike = { type?: unknown; customType?: unknown; data?: unknown };
const CACHE_TTL_CUSTOM_TYPE = "openclaw.cache-ttl";
@@ -24,6 +30,7 @@ type CacheTtlContext = {
modelId?: string;
};
/** Returns whether this provider/model pair supports cache-TTL session markers. */
export function isCacheTtlEligibleProvider(
provider: string,
modelId: string,
@@ -75,6 +82,7 @@ function matchesCacheTtlContext(
return true;
}
/** Reads the most recent cache-TTL marker that matches the optional provider/model context. */
export function readLastCacheTtlTimestamp(
sessionManager: unknown,
context?: CacheTtlContext,
@@ -1,5 +1,11 @@
import { hasAcceptedSessionSpawn } from "../accepted-session-spawn.js";
/**
* Helpers for deciding whether an embedded run produced user-visible or outbound effects.
*
* Fallback and retry code uses these checks to avoid rerunning a model after messages, media,
* cron entries, or spawned sessions have already been delivered.
*/
type AgentPayloadLike = {
text?: unknown;
mediaUrl?: unknown;
@@ -73,6 +79,7 @@ function collectMediaUrlsFromRecord(record: Record<string, unknown>, output: Set
}
}
/** Collects media URLs from agent payloads and committed messaging-tool delivery metadata. */
export function collectDeliveredMediaUrls(result: AgentDeliveryEvidence): string[] {
const urls = new Set<string>();
if (Array.isArray(result.payloads)) {
@@ -88,6 +95,7 @@ export function collectDeliveredMediaUrls(result: AgentDeliveryEvidence): string
return Array.from(urls);
}
/** Collects media URLs recorded by messaging-tool sends and their target attachments. */
export function collectMessagingToolDeliveredMediaUrls(
result: Pick<AgentDeliveryEvidence, "messagingToolSentMediaUrls" | "messagingToolSentTargets">,
): string[] {
@@ -107,6 +115,7 @@ function hasPositiveNumber(value: unknown): boolean {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
/** Extracts a gateway result payload when the response carries delivery evidence fields. */
export function getGatewayAgentResult(response: unknown): AgentDeliveryEvidence | null {
if (!response || typeof response !== "object") {
return null;
@@ -134,6 +143,7 @@ function hasAgentDeliveryEvidenceShape(value: object): boolean {
);
}
/** Returns whether payload metadata contains visible text, media, presentation, or channel data. */
export function hasVisibleAgentPayload(
result: Pick<AgentDeliveryEvidence, "payloads">,
options: { includeErrorPayloads?: boolean; includeReasoningPayloads?: boolean } = {},
@@ -164,12 +174,14 @@ export function hasVisibleAgentPayload(
});
}
/** Returns whether the messaging tool attempted or committed an outbound delivery. */
export function hasMessagingToolDeliveryEvidence(result: AgentDeliveryEvidence): boolean {
return (
result.didSendViaMessagingTool === true || hasCommittedMessagingToolDeliveryEvidence(result)
);
}
/** Returns whether messaging-tool metadata proves committed text, media, or target delivery. */
export function hasCommittedMessagingToolDeliveryEvidence(
result: Pick<
AgentDeliveryEvidence,
@@ -183,6 +195,7 @@ export function hasCommittedMessagingToolDeliveryEvidence(
);
}
/** Returns whether any outbound side effect makes a retry unsafe. */
export function hasOutboundDeliveryEvidence(result: AgentDeliveryEvidence): boolean {
return (
hasMessagingToolDeliveryEvidence(result) ||
@@ -193,6 +206,7 @@ export function hasOutboundDeliveryEvidence(result: AgentDeliveryEvidence): bool
);
}
/** Formats an agent-command delivery failure message from delivery status metadata. */
export function getAgentCommandDeliveryFailure(result: AgentDeliveryEvidence): string | undefined {
const status = result.deliveryStatus?.status;
if (status !== "failed" && status !== "partial_failed") {
@@ -2,6 +2,12 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import { isExecLikeToolName, type ToolErrorSummary } from "../tool-error-summary.js";
import type { EmbeddedRunFailureSignal } from "./types.js";
/**
* Converts terminal tool errors from unattended embedded runs into failure signals.
*
* Cron runs need fatal execution-denied signals so schedulers do not treat blocked shell access as
* a normal silent completion.
*/
const FAILURE_SIGNAL_CODES = ["SYSTEM_RUN_DENIED", "INVALID_REQUEST"] as const;
function resolveFailureSignalCode(
@@ -15,6 +21,7 @@ function resolveFailureSignalCode(
return undefined;
}
/** Resolves fatal cron failure metadata from the last exec-like tool error, if applicable. */
export function resolveEmbeddedRunFailureSignal(params: {
trigger?: string | undefined;
lastToolError?: ToolErrorSummary | undefined;
@@ -1,9 +1,13 @@
import type { Model } from "../../llm/types.js";
/**
* Reads optional context-token metadata from discovered models without widening the core model type.
*/
type AgentModelWithOptionalContextTokens = Model & {
contextTokens?: number;
};
/** Returns finite context-token metadata when a model discovery source provided it. */
export function readAgentModelContextTokens(model: Model | null | undefined): number | undefined {
const value = (model as AgentModelWithOptionalContextTokens | null | undefined)?.contextTokens;
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -6,6 +6,12 @@ import {
} from "../../auto-reply/reply/reply-run-registry.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
/**
* Shared process state for embedded-agent runs, queues, snapshots, and model-switch requests.
*
* The maps are global-singleton backed so reloads and lazy imports inside the same gateway process
* do not split active-run bookkeeping.
*/
export type EmbeddedAgentQueueHandle = {
kind?: "embedded";
queueMessage: (text: string, options?: EmbeddedAgentQueueMessageOptions) => Promise<void>;
@@ -93,6 +99,7 @@ export const EMBEDDED_RUN_MODEL_SWITCH_REQUESTS =
embeddedRunState.modelSwitchRequests ??
(embeddedRunState.modelSwitchRequests = new Map<string, EmbeddedRunModelSwitchRequest>());
/** Counts active embedded runs while including auto-reply registry runs for shared sessions. */
export function getActiveEmbeddedRunCount(): number {
let activeCount = ACTIVE_EMBEDDED_RUNS.size;
for (const sessionId of listActiveReplyRunSessionIds()) {
@@ -103,6 +110,7 @@ export function getActiveEmbeddedRunCount(): number {
return Math.max(activeCount, getActiveReplyRunCount());
}
/** Lists active embedded-run session keys from both embedded and auto-reply registries. */
export function listActiveEmbeddedRunSessionKeys(): string[] {
return [
...new Set([
@@ -112,6 +120,7 @@ export function listActiveEmbeddedRunSessionKeys(): string[] {
].toSorted((a, b) => a.localeCompare(b));
}
/** Lists active embedded-run session ids from all embedded-run lookup maps. */
export function listActiveEmbeddedRunSessionIds(): string[] {
return [
...new Set([
@@ -3,6 +3,12 @@ import { visitObjectContentBlocks } from "../../shared/message-content-blocks.js
import type { StreamFn } from "../runtime/index.js";
import type { MutableAssistantMessageEventStream } from "../stream-compat.js";
/**
* Decodes HTML entities inside streamed tool-call arguments before downstream execution.
*
* Some providers HTML-escape JSON-ish argument strings in tool-call content blocks; this wrapper
* repairs only arguments, preserving user-facing assistant text exactly as emitted.
*/
const HTML_ENTITY_RE = /&(?:amp|lt|gt|quot|apos|#39|#x[0-9a-f]+|#\d+);/i;
function decodeHtmlEntities(value: string): string {
@@ -24,6 +30,7 @@ function decodeHtmlEntities(value: string): string {
.replace(/&#(\d+);/gi, (_, dec: string) => decodeNumericEntity(dec, 10));
}
/** Recursively decodes common HTML entities in string leaves of an object graph. */
export function decodeHtmlEntitiesInObject(value: unknown): unknown {
if (typeof value === "string") {
return HTML_ENTITY_RE.test(value) ? decodeHtmlEntities(value) : value;
@@ -65,6 +72,8 @@ function wrapStreamMessageObjects(
};
const originalAsyncIterator = stream[Symbol.asyncIterator].bind(stream);
// Patch both final result and streamed partial/message events. Tool execution can consume either
// path depending on provider wrapper shape, so one-sided decoding would leave escaped args live.
(stream as { [Symbol.asyncIterator]: typeof originalAsyncIterator })[Symbol.asyncIterator] =
function () {
const iterator = originalAsyncIterator();
@@ -89,6 +98,7 @@ function wrapStreamMessageObjects(
return stream;
}
/** Wraps a stream function so tool-call arguments are decoded before consumers inspect them. */
export function createHtmlEntityToolCallArgumentDecodingWrapper(
baseStreamFn: StreamFn | undefined,
): StreamFn {