refactor: tighten agent harness surfaces

Refactor the agent harness surface after PR #88821 by moving compaction dispatch into its own module, splitting the harness type into explicit capability interfaces, and renaming the private agent-core class declaration to `CoreAgentHarness` while preserving the exported `AgentHarness` contract.

Verification:
- `node scripts/run-vitest.mjs src/agents/harness/selection.test.ts src/agents/command/cli-compaction.test.ts src/agents/embedded-agent-runner/compact.hooks.test.ts packages/agent-core/src/agent-loop.test.ts packages/agent-core/src/harness/messages.test.ts`
- `pnpm build`
- autoreview clean
- `pnpm check:changed` passed on Testbox `tbx_01kt407hq8sv1csm287pdj3fmp`
- PR CI merge state `CLEAN`
This commit is contained in:
Peter Steinberger
2026-06-02 07:20:43 -04:00
committed by GitHub
parent 2d61521bd3
commit b4dfa950b5
17 changed files with 188 additions and 155 deletions

View File

@@ -209,7 +209,7 @@ interface AgentHarnessTurnState<
activeTools: TTool[];
}
export class AgentHarness<
export class CoreAgentHarness<
TSkill extends Skill = Skill,
TPromptTemplate extends PromptTemplate = PromptTemplate,
TTool extends AgentTool = AgentTool,
@@ -1188,6 +1188,8 @@ export class AgentHarness<
}
}
export { CoreAgentHarness as AgentHarness };
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {
return value;

View File

@@ -891,7 +891,7 @@ export interface AgentHarnessOptions<
followUpMode?: QueueMode;
}
export type { AgentHarness } from "./agent-harness.js";
export type { CoreAgentHarness as AgentHarness } from "./agent-harness.js";
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
if (value instanceof Error) {

View File

@@ -26,8 +26,8 @@ import { shouldPreemptivelyCompactBeforePrompt as shouldPreemptivelyCompactBefor
import { resolveLiveToolResultMaxChars as resolveLiveToolResultMaxCharsImpl } from "../embedded-agent-runner/tool-result-truncation.js";
import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js";
import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-recovery.js";
import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/compaction.js";
import { ensureSelectedAgentHarnessPlugin as ensureSelectedAgentHarnessPluginImpl } from "../harness/runtime-plugin.js";
import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/selection.js";
import type { AgentMessage } from "../runtime/index.js";
import { SessionManager } from "../sessions/session-manager.js";
import {

View File

@@ -458,8 +458,11 @@ export async function loadCompactHooksHarness(): Promise<{
};
});
vi.doMock("../harness/selection.js", () => ({
vi.doMock("../harness/compaction.js", () => ({
maybeCompactAgentHarnessSession: maybeCompactAgentHarnessSessionMock,
}));
vi.doMock("../harness/policy.js", () => ({
resolveAgentHarnessPolicy: resolveAgentHarnessPolicyMock,
}));

View File

@@ -23,11 +23,9 @@ import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import { isRecoverableNativeHarnessBindingFailure } from "../harness/compaction-recovery.js";
import { maybeCompactAgentHarnessSession } from "../harness/compaction.js";
import { resolveAgentHarnessPolicy } from "../harness/policy.js";
import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import {
maybeCompactAgentHarnessSession,
resolveAgentHarnessPolicy,
} from "../harness/selection.js";
import { isOpenAIProvider } from "../openai-routing.js";
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
import { DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON } from "./compact-reasons.js";

View File

@@ -79,8 +79,8 @@ import { resolveOpenClawReferencePaths } from "../docs-path.js";
import { ensureSessionHeader } from "../embedded-agent-helpers.js";
import { pickFallbackThinkingLevel } from "../embedded-agent-helpers.js";
import { coerceToFailoverError, describeFailoverError } from "../failover-error.js";
import { resolveAgentHarnessPolicy } from "../harness/policy.js";
import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js";
import { resolveAgentHarnessPolicy } from "../harness/selection.js";
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
import {
applyAuthHeaderOverride,

View File

@@ -0,0 +1,145 @@
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveUserPath } from "../../utils.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import type { CompactEmbeddedAgentSessionParams } from "../embedded-agent-runner/compact.types.js";
import { resolveModelAsync } from "../embedded-agent-runner/model.js";
import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js";
import { getApiKeyForModel } from "../model-auth.js";
import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js";
import { resolveAgentHarnessPolicy as resolveConfiguredAgentHarnessPolicy } from "./policy.js";
import { selectAgentHarness } from "./selection.js";
import type { AgentHarness } from "./types.js";
const log = createSubsystemLogger("agents/harness");
function resolveHarnessCompactIdentity(params: CompactEmbeddedAgentSessionParams): {
agentDir: string;
agentId: string;
} {
const agentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
return {
agentDir: params.agentDir ?? resolveAgentDir(params.config ?? {}, agentIds.sessionAgentId),
agentId: params.agentId ?? agentIds.sessionAgentId,
};
}
async function resolveHarnessCompactApiKey(params: {
agentDir: string;
compactParams: CompactEmbeddedAgentSessionParams;
}): Promise<string | undefined> {
const { agentDir, compactParams } = params;
const existing = compactParams.resolvedApiKey?.trim();
if (existing) {
return existing;
}
if (
!compactParams.authProfileId?.trim() ||
!compactParams.provider?.trim() ||
!compactParams.model?.trim()
) {
return undefined;
}
const workspaceDir = resolveUserPath(compactParams.workspaceDir);
const { model } = await resolveModelAsync(
compactParams.provider,
compactParams.model,
agentDir,
compactParams.config,
{
authProfileId: compactParams.authProfileId,
workspaceDir,
},
);
if (!model) {
return undefined;
}
const apiKeyInfo = await getApiKeyForModel({
model,
cfg: compactParams.config,
profileId: compactParams.authProfileId,
agentDir,
workspaceDir,
});
return apiKeyInfo.apiKey?.trim() || undefined;
}
export async function maybeCompactAgentHarnessSession(
params: CompactEmbeddedAgentSessionParams,
): Promise<EmbeddedAgentCompactResult | undefined> {
if (params.provider && isCliRuntimeProvider(params.provider, { config: params.config })) {
return undefined;
}
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const runtime = resolveConfiguredAgentHarnessPolicy({
provider: params.provider,
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
}).runtime;
if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider, cfg: params.config })) {
return undefined;
}
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const agentHarnessRuntimeOverride =
selectedRuntime && !isDefaultAgentRuntimeId(selectedRuntime) ? selectedRuntime : undefined;
let harness: AgentHarness;
try {
harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride,
});
} catch (err) {
if (agentHarnessRuntimeOverride) {
const message = formatErrorMessage(err);
if (message.includes("does not support")) {
return undefined;
}
}
throw err;
}
if (!harness.compact) {
if (harness.id !== "openclaw") {
return {
ok: false,
compacted: false,
reason: `Agent harness "${harness.id}" does not support compaction.`,
failure: { reason: "unsupported_harness_compaction" },
};
}
return undefined;
}
const compactIdentity = resolveHarnessCompactIdentity(params);
const compactParams = {
...params,
agentDir: compactIdentity.agentDir,
agentId: compactIdentity.agentId,
};
let resolvedApiKey: string | undefined;
try {
resolvedApiKey = await resolveHarnessCompactApiKey({
agentDir: compactIdentity.agentDir,
compactParams,
});
} catch (err) {
log.debug("agent harness compaction credential lookup failed", {
error: formatErrorMessage(err),
});
}
return harness.compact(resolvedApiKey ? { ...compactParams, resolvedApiKey } : compactParams);
}

View File

@@ -8,9 +8,9 @@ import type {
EmbeddedRunAttemptParams,
EmbeddedRunAttemptResult,
} from "../embedded-agent-runner/run/types.js";
import { maybeCompactAgentHarnessSession } from "./compaction.js";
import { clearAgentHarnesses, registerAgentHarness } from "./registry.js";
import {
maybeCompactAgentHarnessSession,
resolveAgentHarnessPolicy,
resolveAvailableAgentHarnessPolicy,
runAgentHarnessAttempt,

View File

@@ -8,25 +8,18 @@ import {
} from "../../infra/diagnostic-trace-context.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
import { resolveUserPath } from "../../utils.js";
import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import {
resolveEffectiveToolPolicy,
resolveGroupToolPolicy,
resolveInheritedToolPolicyForSession,
resolveSubagentToolPolicyForSession,
} from "../agent-tools.policy.js";
import type { CompactEmbeddedAgentSessionParams } from "../embedded-agent-runner/compact.types.js";
import { resolveModelAsync } from "../embedded-agent-runner/model.js";
import type {
EmbeddedRunAttemptParams,
EmbeddedRunAttemptResult,
} from "../embedded-agent-runner/run/types.js";
import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js";
import { getApiKeyForModel } from "../model-auth.js";
import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js";
import { isCliRuntimeAliasForProvider } from "../model-runtime-aliases.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { resolveSenderToolPolicy } from "../sender-tool-policy.js";
import {
@@ -500,134 +493,6 @@ function logAgentHarnessSelection(
});
}
function resolveHarnessCompactIdentity(params: CompactEmbeddedAgentSessionParams): {
agentDir: string;
agentId: string;
} {
const agentIds = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
return {
agentDir: params.agentDir ?? resolveAgentDir(params.config ?? {}, agentIds.sessionAgentId),
agentId: params.agentId ?? agentIds.sessionAgentId,
};
}
async function resolveHarnessCompactApiKey(params: {
agentDir: string;
compactParams: CompactEmbeddedAgentSessionParams;
}): Promise<string | undefined> {
const { agentDir, compactParams } = params;
const existing = compactParams.resolvedApiKey?.trim();
if (existing) {
return existing;
}
if (
!compactParams.authProfileId?.trim() ||
!compactParams.provider?.trim() ||
!compactParams.model?.trim()
) {
return undefined;
}
const workspaceDir = resolveUserPath(compactParams.workspaceDir);
const { model } = await resolveModelAsync(
compactParams.provider,
compactParams.model,
agentDir,
compactParams.config,
{
authProfileId: compactParams.authProfileId,
workspaceDir,
},
);
if (!model) {
return undefined;
}
const apiKeyInfo = await getApiKeyForModel({
model,
cfg: compactParams.config,
profileId: compactParams.authProfileId,
agentDir,
workspaceDir,
});
return apiKeyInfo.apiKey?.trim() || undefined;
}
export async function maybeCompactAgentHarnessSession(
params: CompactEmbeddedAgentSessionParams,
): Promise<EmbeddedAgentCompactResult | undefined> {
if (params.provider && isCliRuntimeProvider(params.provider, { config: params.config })) {
return undefined;
}
const runtimePolicySessionKey = params.sandboxSessionKey ?? params.sessionKey;
const runtimePolicyAgentId =
params.sandboxSessionKey && parseAgentSessionKey(params.sandboxSessionKey)
? undefined
: params.agentId;
const runtime = resolveConfiguredAgentHarnessPolicy({
provider: params.provider,
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
}).runtime;
if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider, cfg: params.config })) {
return undefined;
}
const selectedRuntime = normalizeOptionalAgentRuntimeId(params.agentHarnessId);
const agentHarnessRuntimeOverride =
selectedRuntime && !isDefaultAgentRuntimeId(selectedRuntime) ? selectedRuntime : undefined;
let harness: AgentHarness;
try {
harness = selectAgentHarness({
provider: params.provider ?? "",
modelId: params.model,
config: params.config,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentHarnessRuntimeOverride,
});
} catch (err) {
if (agentHarnessRuntimeOverride) {
const message = formatErrorMessage(err);
if (message.includes("does not support")) {
return undefined;
}
}
throw err;
}
if (!harness.compact) {
if (harness.id !== "openclaw") {
return {
ok: false,
compacted: false,
reason: `Agent harness "${harness.id}" does not support compaction.`,
failure: { reason: "unsupported_harness_compaction" },
};
}
return undefined;
}
const compactIdentity = resolveHarnessCompactIdentity(params);
const compactParams = {
...params,
agentDir: compactIdentity.agentDir,
agentId: compactIdentity.agentId,
};
let resolvedApiKey: string | undefined;
try {
resolvedApiKey = await resolveHarnessCompactApiKey({
agentDir: compactIdentity.agentDir,
compactParams,
});
} catch (err) {
log.debug("agent harness compaction credential lookup failed", {
error: formatErrorMessage(err),
});
}
return harness.compact(resolvedApiKey ? { ...compactParams, resolvedApiKey } : compactParams);
}
function formatProviderModel(params: { provider: string; modelId?: string }): string {
return params.modelId ? `${params.provider}/${params.modelId}` : params.provider;
}

View File

@@ -65,7 +65,7 @@ export type AgentHarnessDeliveryDefaults = {
sourceVisibleReplies?: "automatic" | "message_tool";
};
export type AgentHarness = {
export type AgentHarnessRunCapability = {
id: string;
label: string;
pluginId?: string;
@@ -78,16 +78,34 @@ export type AgentHarness = {
deliveryDefaults?: AgentHarnessDeliveryDefaults;
supports(ctx: AgentHarnessSupportContext): AgentHarnessSupport;
runAttempt(params: AgentHarnessAttemptParams): Promise<AgentHarnessAttemptResult>;
};
export type AgentHarnessSideQuestionCapability = {
runSideQuestion?(params: AgentHarnessSideQuestionParams): Promise<AgentHarnessSideQuestionResult>;
};
export type AgentHarnessClassificationCapability = {
classify?(
result: AgentHarnessAttemptResult,
ctx: AgentHarnessAttemptParams,
): AgentHarnessResultClassification | undefined;
};
export type AgentHarnessCompactionCapability = {
compact?(params: AgentHarnessCompactParams): Promise<AgentHarnessCompactResult | undefined>;
};
export type AgentHarnessSessionLifecycleCapability = {
reset?(params: AgentHarnessResetParams): Promise<void> | void;
dispose?(): Promise<void> | void;
};
export type AgentHarness = AgentHarnessRunCapability &
AgentHarnessSideQuestionCapability &
AgentHarnessClassificationCapability &
AgentHarnessCompactionCapability &
AgentHarnessSessionLifecycleCapability;
export type RegisteredAgentHarness = {
harness: AgentHarness;
ownerPluginId?: string;

View File

@@ -41,8 +41,8 @@ import { sanitizeUserFacingText } from "../../agents/embedded-agent-helpers/sani
import { isMessagingToolSendAction } from "../../agents/embedded-agent-messaging.js";
import { runEmbeddedAgent } from "../../agents/embedded-agent.js";
import { isFailoverError } from "../../agents/failover-error.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import { ensureSelectedAgentHarnessPlugin } from "../../agents/harness/runtime-plugin.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js";
import { isMissingProviderAuthError } from "../../agents/model-auth.js";
import { runWithModelFallback, isFallbackSummaryError } from "../../agents/model-fallback.js";

View File

@@ -6,7 +6,7 @@ import {
} from "@openclaw/normalization-core/string-coerce";
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import {
OPENAI_CODEX_PROVIDER_ID,
OPENAI_PROVIDER_ID,

View File

@@ -5,7 +5,7 @@ import {
import { normalizeOptionalAgentRuntimeId } from "../../agents/agent-runtime-id.js";
import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles.js";
import type { AuthProfileCredential } from "../../agents/auth-profiles/types.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import {
type ModelAliasIndex,
buildConfiguredModelCatalog,

View File

@@ -4,7 +4,7 @@ import {
resolveSessionAgentId,
} from "../../agents/agent-scope.js";
import { resolveCliRuntimeModelBackendBinding } from "../../agents/cli-backends.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import type { ModelCatalogEntry } from "../../agents/model-catalog.js";
import { normalizeProviderId, type ModelAliasIndex } from "../../agents/model-selection.js";
import { resolveContextConfigProviderForRuntime } from "../../agents/openai-routing.js";

View File

@@ -13,7 +13,7 @@ import { resolveEmbeddedFullAccessState } from "../../agents/embedded-agent-runn
import type { EmbeddedFullAccessBlockedReason } from "../../agents/embedded-agent-runner/types.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import { runAgentHarnessBeforeMessageWriteHook } from "../../agents/harness/hook-helpers.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-routing.js";
import { resolveIngressWorkspaceOverrideForSpawnedRun } from "../../agents/spawned-context.js";
import type { SilentReplyPromptMode } from "../../agents/system-prompt.types.js";

View File

@@ -5,7 +5,7 @@ import {
import { clearSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import { resolveContextTokensForModel } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import type { ModelCatalogEntry } from "../../agents/model-catalog.js";
import { parseConfiguredModelVisibilityEntries } from "../../agents/model-selection-shared.js";
import {

View File

@@ -1,7 +1,7 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js";
import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js";
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-routing.js";
import { expandToolGroups, normalizeToolName } from "../../agents/tool-policy.js";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
@@ -1196,7 +1196,9 @@ async function disposeCronRunContext(params: {
sessionId: params.sessionId,
reason: "isolated-cron-dispose",
onError: (error, sid) => {
logWarn(`[cron] Failed to retire MCP runtime during isolated cron dispose ${sid}: ${String(error)}`);
logWarn(
`[cron] Failed to retire MCP runtime during isolated cron dispose ${sid}: ${String(error)}`,
);
},
}).catch(() => {});
}