mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 18:24:00 +00:00
docs: document agent diagnostics helpers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user