docs: document agent diagnostics helpers

This commit is contained in:
Peter Steinberger
2026-06-03 22:42:05 -04:00
parent 3788a2fd3d
commit 8f7808d1e6
6 changed files with 47 additions and 0 deletions

View File

@@ -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<string, unknown> | 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,

View File

@@ -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<T>(
params: ExecuteWithApiKeyRotationOptions<T>,
): Promise<T> {
@@ -61,6 +68,8 @@ export async function executeWithApiKeyRotation<T>(
: 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<T>(
}
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<T>(
}
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;
}

View File

@@ -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,

View File

@@ -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[];

View File

@@ -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,

View File

@@ -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<NormalizedSpawnedRunMetadata, "groupId" | "groupChannel" | "groupSpace" | "workspaceDir"> {
@@ -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<SpawnedRunMetadata, "spawnedBy" | "workspaceDir"> | null,
): string | undefined {