From 8f7808d1e6c33ea03273cb0a017ab33987e74567 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Jun 2026 22:42:05 -0400 Subject: [PATCH] docs: document agent diagnostics helpers --- src/agents/anthropic-payload-log.ts | 10 ++++++++++ src/agents/api-key-rotation.ts | 13 +++++++++++++ src/agents/codex-native-web-search.ts | 5 +++++ src/agents/embedded-agent-payloads.ts | 2 ++ src/agents/identity-avatar.ts | 10 ++++++++++ src/agents/spawned-context.ts | 7 +++++++ 6 files changed, 47 insertions(+) diff --git a/src/agents/anthropic-payload-log.ts b/src/agents/anthropic-payload-log.ts index 72982cbbe336..facaac59a758 100644 --- a/src/agents/anthropic-payload-log.ts +++ b/src/agents/anthropic-payload-log.ts @@ -10,6 +10,9 @@ import { sanitizeDiagnosticPayload } from "./payload-redaction.js"; import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js"; import type { AgentMessage, StreamFn } from "./runtime/index.js"; +// Optional Anthropic diagnostics logger. Payload and error data is redacted +// before JSONL output; payload digests let operators correlate requests without +// keeping raw secret-bearing content. type PayloadLogStage = "request" | "usage"; type PayloadLogEvent = { @@ -70,6 +73,8 @@ function formatError(error: unknown): string | undefined { } function digest(value: unknown): string | undefined { + // Hash the redacted payload so repeated requests can be correlated even when + // payload bodies are too sensitive to inspect directly. const serialized = safeJsonStringify(value); if (!serialized) { return undefined; @@ -82,6 +87,8 @@ function isAnthropicModel(model: Model | undefined | null): boolean { } function findLastAssistantUsage(messages: AgentMessage[]): Record | null { + // Usage is attached to assistant messages after streaming; walk backwards to + // avoid logging stale usage from an earlier assistant turn. for (let i = messages.length - 1; i >= 0; i -= 1) { const msg = messages[i] as { role?: unknown; usage?: unknown }; if (msg?.role === "assistant" && msg.usage && typeof msg.usage === "object") { @@ -97,6 +104,7 @@ type AnthropicPayloadLogger = { recordUsage: (messages: AgentMessage[], error?: unknown) => void; }; +/** Create an Anthropic payload/usage logger when the env flag is enabled. */ export function createAnthropicPayloadLogger(params: { env?: NodeJS.ProcessEnv; runId?: string; @@ -139,6 +147,8 @@ export function createAnthropicPayloadLogger(params: { return streamFn(model, context, options); } const nextOnPayload = (payload: unknown) => { + // Forward the original payload to the provider hook, but persist only + // the redacted diagnostic copy. const redactedPayload = sanitizeDiagnosticPayload(payload); record({ ...base, diff --git a/src/agents/api-key-rotation.ts b/src/agents/api-key-rotation.ts index 150b4f33c73c..82e015ad95b1 100644 --- a/src/agents/api-key-rotation.ts +++ b/src/agents/api-key-rotation.ts @@ -10,6 +10,8 @@ import { } from "../provider-runtime/operation-retry.js"; import { collectProviderApiKeys, isApiKeyRateLimitError } from "./live-auth-keys.js"; +// API-key rotation wrapper for provider calls. It tries configured keys in order +// on rate-limit-like failures and can also retry transient errors on the same key. type ApiKeyRetryParams = { apiKey: string; error: unknown; @@ -29,6 +31,7 @@ function dedupeApiKeys(raw: string[]): string[] { return normalizeUniqueStringEntries(raw); } +/** Collect primary and live-discovered provider keys in stable de-duped order. */ export function collectProviderApiKeysForExecution(params: { provider: string; primaryApiKey?: string; @@ -37,6 +40,10 @@ export function collectProviderApiKeysForExecution(params: { return dedupeApiKeys([primaryApiKey?.trim() ?? "", ...collectProviderApiKeys(provider)]); } +/** + * Execute a provider operation with key rotation and optional same-key transient + * retries. + */ export async function executeWithApiKeyRotation( params: ExecuteWithApiKeyRotationOptions, ): Promise { @@ -61,6 +68,8 @@ export async function executeWithApiKeyRotation( : isApiKeyRateLimitError(message); if (rotateKey) { + // A rotation signal consumes the current key and moves to the next key + // without running same-key transient retry logic. if (apiKeyIndex + 1 >= keys.length) { break; } @@ -84,6 +93,8 @@ export async function executeWithApiKeyRotation( } const delayMs = resolveTransientProviderDelayMs(transientRetry, attemptNumber); + // Same-key transient retries are bounded by provider policy and keep the + // current key stable so auth rotation only handles key-specific failures. const sleep = transientRetry.sleep ?? sleepWithAbort; await sleep(delayMs, transientRetry.signal); } @@ -97,6 +108,8 @@ export async function executeWithApiKeyRotation( } function toLintErrorObject(value: unknown, fallbackMessage: string): Error { + // Preserve thrown object properties for callers/tests while still satisfying + // Error-only throw lint expectations. if (value instanceof Error) { return value; } diff --git a/src/agents/codex-native-web-search.ts b/src/agents/codex-native-web-search.ts index 148cb5c41227..282156167adc 100644 --- a/src/agents/codex-native-web-search.ts +++ b/src/agents/codex-native-web-search.ts @@ -5,6 +5,9 @@ import { } from "./codex-native-web-search-core.js"; import { resolveCodexNativeWebSearchConfig } from "./codex-native-web-search.shared.js"; import { resolveDefaultModelForAgent } from "./model-selection.js"; + +// Public Codex native web-search facade. It exports the core activation helpers +// and answers whether the feature is relevant for the configured agent model. export { buildCodexNativeWebSearchTool, patchCodexNativeWebSearchPayload, @@ -36,6 +39,8 @@ export function isCodexNativeWebSearchRelevant(params: { const configuredModelApi = configuredProvider?.models?.find( (candidate) => candidate.id === defaultModel.model, )?.api; + // If explicit config/auth did not opt in, model API eligibility can still make + // native search relevant for Codex-routable defaults. return isCodexNativeSearchEligibleModel({ modelProvider: defaultModel.provider, modelApi: configuredModelApi ?? configuredProvider?.api, diff --git a/src/agents/embedded-agent-payloads.ts b/src/agents/embedded-agent-payloads.ts index fe89378965ab..fc72e4ef848f 100644 --- a/src/agents/embedded-agent-payloads.ts +++ b/src/agents/embedded-agent-payloads.ts @@ -1,3 +1,5 @@ +// Channel-facing reply payload emitted by embedded agents. Keep this type small: +// channel adapters decide how to render text/media/reply targeting. export type BlockReplyPayload = { text?: string; mediaUrls?: string[]; diff --git a/src/agents/identity-avatar.ts b/src/agents/identity-avatar.ts index 4179e160ab34..05ef92b17327 100644 --- a/src/agents/identity-avatar.ts +++ b/src/agents/identity-avatar.ts @@ -17,6 +17,9 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.j import { loadAgentIdentityFromWorkspace } from "./identity-file.js"; import { resolveAgentIdentity } from "./identity.js"; +// Agent avatar resolution for UI/public surfaces. Remote/data sources are +// allowed directly; local files must stay inside the agent workspace and satisfy +// shared avatar policy limits. export type AgentAvatarResolution = | { kind: "none"; reason: string; source?: string } | { kind: "local"; filePath: string; source: string } @@ -40,6 +43,8 @@ function resolveAvatarSource( const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg)); const fromUiConfig = normalizeOptionalString(cfg.ui?.assistant?.avatar) ?? null; if (opts?.includeUiOverride) { + // UI override only wins for the default agent unless callers explicitly ask + // for it as a final fallback for non-default agents. if (normalizedAgentId === defaultAgentId && fromUiConfig) { return fromUiConfig; } @@ -77,6 +82,8 @@ function resolveLocalAvatarPath(params: { ? resolveUserPath(raw) : path.resolve(workspaceRoot, raw); const realPath = resolveExistingPath(resolved); + // Resolve symlinks before the workspace check so local avatar paths cannot + // escape the workspace through link traversal. if (!isPathWithinRoot(workspaceRoot, realPath)) { return { ok: false, reason: "outside_workspace" }; } @@ -112,6 +119,7 @@ function isSafeRelativeAvatarSource(source: string): boolean { return parts.every((part) => part !== ".."); } +/** Return a safe public description of the configured avatar source. */ export function resolvePublicAgentAvatarSource( resolved: AgentAvatarPublicSourceInput, ): string | undefined { @@ -120,6 +128,7 @@ export function resolvePublicAgentAvatarSource( return undefined; } if (isAvatarDataUrl(source)) { + // Data URLs can be large and sensitive; expose only the media/header prefix. const commaIndex = source.indexOf(","); const header = commaIndex > 0 @@ -133,6 +142,7 @@ export function resolvePublicAgentAvatarSource( return isSafeRelativeAvatarSource(source) ? source : undefined; } +/** Resolve the effective avatar for an agent, including config and IDENTITY.md. */ export function resolveAgentAvatar( cfg: OpenClawConfig, agentId: string, diff --git a/src/agents/spawned-context.ts b/src/agents/spawned-context.ts index a71f7e69c70d..e8dafb6d738c 100644 --- a/src/agents/spawned-context.ts +++ b/src/agents/spawned-context.ts @@ -3,6 +3,9 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; import { resolveAgentWorkspaceDir } from "./agent-scope.js"; +// Normalized metadata passed from session-spawn tools into child run records. +// It captures lineage, group routing, workspace inheritance, and inherited tool +// policy without exposing the broader tool context object. export type SpawnedRunMetadata = { spawnedBy?: string | null; groupId?: string | null; @@ -29,6 +32,7 @@ type NormalizedSpawnedRunMetadata = { workspaceDir?: string; }; +/** Normalize optional spawn metadata fields from persisted or tool-provided input. */ export function normalizeSpawnedRunMetadata( value?: SpawnedRunMetadata | null, ): NormalizedSpawnedRunMetadata { @@ -41,6 +45,7 @@ export function normalizeSpawnedRunMetadata( }; } +/** Project tool runtime context down to the persisted spawned-run metadata shape. */ export function mapToolContextToSpawnedRunMetadata( value?: SpawnedToolContext | null, ): Pick { @@ -52,6 +57,7 @@ export function mapToolContextToSpawnedRunMetadata( }; } +/** Resolve which workspace a spawned run should inherit. */ export function resolveSpawnedWorkspaceInheritance(params: { config: OpenClawConfig; targetAgentId?: string; @@ -71,6 +77,7 @@ export function resolveSpawnedWorkspaceInheritance(params: { return agentId ? resolveAgentWorkspaceDir(params.config, normalizeAgentId(agentId)) : undefined; } +/** Return a spawned run's ingress workspace override only for child runs. */ export function resolveIngressWorkspaceOverrideForSpawnedRun( metadata?: Pick | null, ): string | undefined {