diff --git a/src/agents/embedded-agent-runner/run.ts b/src/agents/embedded-agent-runner/run.ts index 835fa8444412..6454ace13e29 100644 --- a/src/agents/embedded-agent-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -35,7 +35,6 @@ import { formatErrorMessage, toErrorObject } from "../../infra/errors.js"; import { redactIdentifier } from "../../logging/redact-identifier.js"; import { buildAgentHookContextChannelFields } from "../../plugins/hook-agent-context.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; -import { resolveProviderAuthProfileId } from "../../plugins/provider-runtime.js"; import { looksLikeSecretSentinel, resolveSecretSentinel } from "../../secrets/sentinel.js"; import { createAgentHarnessTaskRuntimeScope } from "../../tasks/agent-harness-task-runtime-scope.js"; import { createTrajectoryRuntimeRecorder } from "../../trajectory/runtime.js"; @@ -59,7 +58,6 @@ import { markAuthProfileFailure, markAuthProfileSuccess, } from "../auth-profiles.js"; -import { resolveExternalCliAuthOverlayScopeFromSelection } from "../auth-profiles/external-cli-auth-selection.js"; import { listActiveProcessSessionReferences } from "../bash-process-references.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { @@ -94,31 +92,14 @@ import { FailoverError, resolveFailoverStatus, } from "../failover-error.js"; -import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js"; -import { - agentHarnessBuildsOpenClawTools, - selectAgentHarness, - selectAgentHarnessForPreparedModelProviders, -} from "../harness/selection.js"; -import { - resolveAgentHarnessPreparedAuthSupport, - resolveAgentHarnessPreparedRouteSupport, -} from "../harness/support.js"; +import { agentHarnessBuildsOpenClawTools } from "../harness/selection.js"; import { LiveSessionModelSwitchError } from "../live-model-switch-error.js"; import { shouldSwitchToLiveModel, clearLiveModelSwitchPending } from "../live-model-switch.js"; import { applyAuthHeaderOverride, applyLocalNoAuthHeaderOverride, - ensureAuthProfileStore, - ensureAuthProfileStoreWithoutExternalProfiles, type ResolvedProviderAuth, } from "../model-auth.js"; -import { ensureOpenClawModelsJson } from "../models-config.js"; -import { - OPENAI_PROVIDER_ID, - resolveContextConfigProviderForRuntime, - resolveSelectedOpenAIRuntimeProvider, -} from "../openai-routing.js"; import { hasOnlyAssistantReasoningContent } from "../replay-turn-classification.js"; import { runAgentCleanupStep } from "../run-cleanup-timeout.js"; import { @@ -128,14 +109,11 @@ import { import { createAgentRunDirectAbortError } from "../run-termination.js"; import { buildAgentRuntimePlan } from "../runtime-plan/build.js"; import { - createPreparedRuntimeModelMaterializer, hasPreparedAuthAttemptModelMetadata, - providerUsesCredentialScopedModelMetadata, resolveCredentialScopedAuthAttemptModelDecision, } from "../runtime-plan/credential-scoped-model.js"; import { canRunPreparedAgentRuntimeAuthAttempt, - prepareAgentRuntimeAuth, type PreparedAgentRuntimeAuthAttempt, } from "../runtime-plan/prepare-auth.js"; import type { AgentRuntimeAuthPlan } from "../runtime-plan/types.js"; @@ -168,7 +146,6 @@ import { import { resolveEmbeddedRunFailureSignal } from "./failure-signal.js"; import { resolveGlobalLane, resolveSessionLane } from "./lanes.js"; import { log } from "./logger.js"; -import { createEmptyAgentDiscoveryStores, resolveModelAsync } from "./model.js"; import { createPostCompactionLoopGuard, PostCompactionLoopPersistedError, @@ -189,6 +166,7 @@ import { createEmbeddedRunAuthController, resolveEmbeddedAuthCooldownProbePolicy, } from "./run/auth-controller.js"; +import { prepareEmbeddedRunAuthPlan } from "./run/auth-plan.js"; import { resolveAuthProfileFailureReason } from "./run/auth-profile-failure-policy.js"; import { createScopedAuthProfileStore, resolveAttemptDispatchApiKey } from "./run/auth-store.js"; import { runEmbeddedAttemptWithBackend } from "./run/backend.js"; @@ -246,6 +224,12 @@ import { EMBEDDED_RUN_LANE_TIMEOUT_GRACE_MS, resolveEmbeddedRunLaneTimeoutMs, } from "./run/lane-runtime.js"; +import { + resolveEmbeddedRunEffectiveModel, + selectEmbeddedRunHarness, + selectEmbeddedRunHarnessForPreparedAttempts, +} from "./run/model-harness.js"; +import { resolveEmbeddedRunModelSetup } from "./run/model-setup.js"; import type { RunEmbeddedAgentParams } from "./run/params.js"; import { buildEmbeddedRunPayloads } from "./run/payloads.js"; import { createEmbeddedRunProgressController } from "./run/progress-controller.js"; @@ -260,7 +244,6 @@ import { resolveAttemptTrajectoryAttribution, resolveInitialEmbeddedRunModel, resolveInitialThinkLevel, - resolveRequestStreamTransportOverrides, } from "./run/runtime-resolution.js"; import { assertAgentHarnessRunAdmission, @@ -269,13 +252,6 @@ import { isNoRealConversationCompactionNoop, resetNoRealConversationTokenSnapshot, } from "./run/session-bootstrap.js"; -import { - buildBeforeModelResolveAttachments, - createNativeModelOwnedRuntimeModel, - resolveEmbeddedRuntimeModelPolicy, - resolveHookModelSelection, - resolveNativeModelOwnedHarnessId, -} from "./run/setup.js"; import { resolveSkillWorkshopAttemptParams } from "./run/skill-workshop-attempt-params.js"; import { isEmbeddedRunTerminalAbort, @@ -553,180 +529,46 @@ async function runEmbeddedAgentInternal( notifyExecutionPhase("runtime_plugins", { provider, model: modelId }); } - const hookSelection = await resolveHookModelSelection({ - prompt: params.prompt, - attachments: buildBeforeModelResolveAttachments(params.images), + const modelSetup = await resolveEmbeddedRunModelSetup({ + runParams: params, provider, modelId, - modelSelectionLocked: params.modelSelectionLocked, + agentDir, + workspaceDir: resolvedWorkspace, + globalLane, hookRunner, hookContext: hookCtx, + onHooksResolved: () => startupStages.mark("hooks"), }); - const modelSelectionChangedByHook = - hookSelection.provider !== provider || hookSelection.modelId !== modelId; - provider = hookSelection.provider; - modelId = hookSelection.modelId; - const requestedModelId = modelId; - const beforeAgentStartResult = hookSelection.beforeAgentStartResult; - const requestStreamTransportOverrides = resolveRequestStreamTransportOverrides( - params.streamParams, - ); - startupStages.mark("hooks"); - await ensureSelectedAgentHarnessPlugin({ - provider, - modelId, - config: params.config, - agentId: params.agentId, - sessionKey: params.sessionKey, - agentHarnessId: params.agentHarnessId, - agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, - requestTransportOverrides: requestStreamTransportOverrides, - workspaceDir: resolvedWorkspace, - }); - let agentHarness = selectAgentHarness({ - provider, - modelId, - ...(requestStreamTransportOverrides - ? { - modelProvider: { - requestTransportOverrides: requestStreamTransportOverrides, - }, - } - : {}), - config: params.config, - agentId: params.agentId, - sessionKey: params.sessionKey, - agentHarnessId: params.agentHarnessId, - agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, - }); - let pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; - const expectedHarnessArtifact = params.expectedAgentHarnessRuntimeArtifact; - if (expectedHarnessArtifact && expectedHarnessArtifact.harnessId !== agentHarness.id) { - throw new Error( - `Verified inference requires agent harness ${expectedHarnessArtifact.harnessId}, but ${agentHarness.id} was selected.`, - ); - } - if (expectedHarnessArtifact && !agentHarness.runtimeArtifact) { - throw new Error( - `Agent harness ${agentHarness.id} cannot attest the verified inference runtime artifact.`, - ); - } - const nativeModelOwnedHarnessId = resolveNativeModelOwnedHarnessId({ - agentHarnessId: params.agentHarnessId, - modelSelectionLocked: params.modelSelectionLocked, - selectedHarnessId: agentHarness.id, - }); - const nativeModelOwned = nativeModelOwnedHarnessId !== undefined; - const modelConfigProvider = provider; - let resolvedModelProvider = provider; - let firstModelResolution: Awaited> | undefined; - let modelResolution: Awaited> | undefined; - if (nativeModelOwned) { - modelResolution = { - model: createNativeModelOwnedRuntimeModel({ provider, modelId }), - ...createEmptyAgentDiscoveryStores(), - }; - } else { - const selectedRuntimeProvider = resolveSelectedOpenAIRuntimeProvider({ - provider, - harnessRuntime: agentHarness.id, - agentHarnessId: agentHarness.id, - authProfileProvider: params.authProfileId?.split(":", 1)[0], - authProfileId: params.authProfileId, - config: params.config, - workspaceDir: resolvedWorkspace, - }); - const modelResolutionProviders = - selectedRuntimeProvider !== provider ? [selectedRuntimeProvider, provider] : [provider]; - for (const candidateProvider of modelResolutionProviders) { - const candidateResolution = await resolveModelAsync( - candidateProvider, - modelId, - agentDir, - params.config, - { - // Plugin dynamic model hooks can resolve explicit model refs without - // first generating OpenClaw models.json. This keeps one-shot model runs from - // blocking on unrelated provider discovery. - skipAgentDiscovery: true, - allowBundledStaticCatalogFallback: pluginHarnessOwnsTransport, - preferBundledStaticCatalogTransport: pluginHarnessOwnsTransport, - workspaceDir: resolvedWorkspace, - authProfileId: params.authProfileId, - }, - ); - firstModelResolution ??= candidateResolution; - if (candidateResolution.model) { - resolvedModelProvider = candidateProvider; - modelResolution = candidateResolution; - break; - } - } - if (!modelResolution && pluginHarnessOwnsTransport) { - modelResolution ??= firstModelResolution; - } - if (!modelResolution) { - await ensureOpenClawModelsJson(params.config, agentDir, { - workspaceDir: resolvedWorkspace, - }); - for (const candidateProvider of modelResolutionProviders) { - const candidateResolution = await resolveModelAsync( - candidateProvider, - modelId, - agentDir, - params.config, - { - workspaceDir: resolvedWorkspace, - authProfileId: params.authProfileId, - // Enable bundled static catalog fallback so plugin-provided - // models that are not discoverable via agent model discovery - // can still be resolved from the static catalog. - allowBundledStaticCatalogFallback: true, - }, - ); - firstModelResolution ??= candidateResolution; - if (candidateResolution.model) { - resolvedModelProvider = candidateProvider; - modelResolution = candidateResolution; - break; - } - } - } - modelResolution ??= firstModelResolution; - } - if (!modelResolution) { - throw new FailoverError(`Unknown model: ${provider}/${modelId}`, { - reason: "model_not_found", - provider, - model: modelId, - sessionId: params.sessionId, - lane: globalLane, - }); - } - provider = resolvedModelProvider; - const { model, error, authStorage, modelRegistry } = modelResolution; - if (!model) { - throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, { - reason: "model_not_found", - provider, - model: modelId, - sessionId: params.sessionId, - lane: globalLane, - }); - } + provider = modelSetup.provider; + modelId = modelSetup.modelId; + const { + requestedModelId, + modelSelectionChangedByHook, + beforeAgentStartResult, + requestStreamTransportOverrides, + expectedHarnessArtifact, + nativeModelOwnedHarnessId, + nativeModelOwned, + modelConfigProvider, + model, + authStorage, + modelRegistry, + } = modelSetup; + let agentHarness = modelSetup.agentHarness; + let pluginHarnessOwnsTransport = modelSetup.pluginHarnessOwnsTransport; let runtimeModel = model; const resolveEffectiveModel = (candidate: typeof runtimeModel) => - resolveEmbeddedRuntimeModelPolicy({ - cfg: params.config, + resolveEmbeddedRunEffectiveModel({ + runParams: params, provider, - contextConfigProvider: resolveContextConfigProviderForRuntime({ - provider: modelConfigProvider, - runtimeId: agentHarness.id, - config: params.config, - }), + modelConfigProvider, modelId, + agentHarnessId: agentHarness.id, runtimeModel: candidate, nativeModelOwned, + requestStreamTransportOverrides, + nativeModelOwnedHarnessId, }); const initialResolvedRuntimeModel = resolveEffectiveModel(runtimeModel); let contextTokenBudget = initialResolvedRuntimeModel.contextTokenBudget; @@ -745,82 +587,34 @@ async function runEmbeddedAgentInternal( outerContextTokenMeta = contextTokenBudget === undefined ? {} : { contextTokens: contextTokenBudget }; }; - const buildHarnessModelProvider = ( - candidate: typeof effectiveModel, - plan?: AgentRuntimeAuthPlan, - preparedAuthAttempt?: PreparedAgentRuntimeAuthAttempt, - ) => { - const route = plan?.modelRoute; - const routeSupport = resolveAgentHarnessPreparedRouteSupport(plan); - const requestTransportOverrides = - requestStreamTransportOverrides ?? routeSupport.requestTransportOverrides; - return { - api: route?.api ?? candidate.api, - baseUrl: route?.baseUrl ?? candidate.baseUrl, - ...(requestTransportOverrides ? { requestTransportOverrides } : {}), - ...(routeSupport.runtimePolicy ? { runtimePolicy: routeSupport.runtimePolicy } : {}), - ...(plan - ? { - preparedAuth: resolveAgentHarnessPreparedAuthSupport({ - plan, - ...(preparedAuthAttempt?.kind === "profile" || - preparedAuthAttempt?.kind === "direct" - ? { source: preparedAuthAttempt.kind } - : {}), - }), - } - : {}), - }; - }; const selectHarnessForModel = ( candidate: typeof effectiveModel, plan?: AgentRuntimeAuthPlan, preparedAuthAttempt?: PreparedAgentRuntimeAuthAttempt, - ) => { - const selected = selectAgentHarness({ + ) => + selectEmbeddedRunHarness({ + runParams: params, provider, modelId, - modelProvider: buildHarnessModelProvider(candidate, plan, preparedAuthAttempt), - config: params.config, - agentId: params.agentId, - sessionKey: params.sessionKey, - agentHarnessId: params.agentHarnessId, - agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, + model: candidate, + plan, + preparedAuthAttempt, + requestStreamTransportOverrides, + nativeModelOwnedHarnessId, }); - if (nativeModelOwnedHarnessId && selected.id !== nativeModelOwnedHarnessId) { - throw new Error( - `Prepared model route changed the session-pinned agent harness from "${nativeModelOwnedHarnessId}" to "${selected.id}".`, - ); - } - return selected; - }; const selectHarnessForPreparedAttempts = ( candidate: typeof effectiveModel, attempts: readonly PreparedAgentRuntimeAuthAttempt[], - ) => { - const selected = selectAgentHarnessForPreparedModelProviders({ + ) => + selectEmbeddedRunHarnessForPreparedAttempts({ + runParams: params, provider, modelId, - modelProviders: attempts.map((attempt) => { - const route = attempt.plan.modelRoute; - const attemptModel = route - ? { ...candidate, api: route.api, baseUrl: route.baseUrl } - : candidate; - return buildHarnessModelProvider(attemptModel, attempt.plan, attempt); - }), - config: params.config, - agentId: params.agentId, - sessionKey: params.sessionKey, - agentHarnessId: params.agentHarnessId, - agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, + model: candidate, + attempts, + requestStreamTransportOverrides, + nativeModelOwnedHarnessId, }); - if (nativeModelOwnedHarnessId && selected.id !== nativeModelOwnedHarnessId) { - throw new Error( - `Prepared auth routes changed the session-pinned agent harness from "${nativeModelOwnedHarnessId}" to "${selected.id}".`, - ); - } - return selected; - }; startupStages.mark("model-resolution"); notifyExecutionPhase("model_resolution", { provider, model: modelId }); @@ -831,154 +625,39 @@ async function runEmbeddedAgentInternal( pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; const authStages = log.isEnabled("trace") ? createEmbeddedRunStageTracker() : undefined; - const usesOpenAIAuthRouting = provider === OPENAI_PROVIDER_ID; - const openClawNativeCodexResponsesNeedsAuthBootstrap = - !pluginHarnessOwnsTransport && - provider === OPENAI_PROVIDER_ID && - effectiveModel.api === "openai-chatgpt-responses"; - let piExternalCliAuthScope = pluginHarnessOwnsTransport - ? { ignoreAutoPreferredProfile: false } - : openClawNativeCodexResponsesNeedsAuthBootstrap - ? { - providerIds: [OPENAI_PROVIDER_ID], - ignoreAutoPreferredProfile: false, - } - : resolveExternalCliAuthOverlayScopeFromSelection({ - provider, - cfg: params.config, - agentId: params.agentId, - modelId, - workspaceDir: resolvedWorkspace, - userLockedAuthProfileId: - params.authProfileIdSource === "user" ? params.authProfileId : undefined, - }); - let noExternalAuthStore: AuthProfileStore | undefined; - if (!pluginHarnessOwnsTransport && !piExternalCliAuthScope.providerIds) { - noExternalAuthStore = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { - allowKeychainPrompt: false, - }); - piExternalCliAuthScope = resolveExternalCliAuthOverlayScopeFromSelection({ - provider, - cfg: params.config, - agentId: params.agentId, - modelId, - workspaceDir: resolvedWorkspace, - store: noExternalAuthStore, - userLockedAuthProfileId: - params.authProfileIdSource === "user" ? params.authProfileId : undefined, - }); - } - authStages?.mark("scope"); - const attemptAuthProfileStore = usesOpenAIAuthRouting - ? ensureAuthProfileStore(agentDir, { - externalCliProviderIds: [OPENAI_PROVIDER_ID], - allowKeychainPrompt: false, - }) - : pluginHarnessOwnsTransport - ? ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { - allowKeychainPrompt: false, - }) - : piExternalCliAuthScope.providerIds - ? ensureAuthProfileStore(agentDir, { - externalCliProviderIds: piExternalCliAuthScope.providerIds, - allowKeychainPrompt: false, - }) - : (noExternalAuthStore ?? - ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { - allowKeychainPrompt: false, - })); - authStages?.mark("store"); - const requestedProfileId = params.authProfileId?.trim() || undefined; - const lockedProfileId = - params.authProfileIdSource === "user" ? requestedProfileId : undefined; - const preferredProfileId = - piExternalCliAuthScope.ignoreAutoPreferredProfile && !lockedProfileId - ? undefined - : requestedProfileId; - const createAuthPreparation = () => - prepareAgentRuntimeAuth({ - provider, - modelId, - modelApi: model.api, - modelBaseUrl: model.baseUrl, - requestTransportOverrides: requestStreamTransportOverrides, - config: params.config, - env: process.env, - agentDir, - workspaceDir: resolvedWorkspace, - authProfileStore: attemptAuthProfileStore, - sessionAuthProfileId: preferredProfileId, - sessionAuthProfileSource: params.authProfileIdSource, - harnessId: agentHarness.id, - harnessRuntime: agentHarness.id, - harnessAuthBootstrap: agentHarness.authBootstrap, - allowHarnessAuthProfileForwarding: true, - allowTransientCooldownProbe: params.allowTransientCooldownProbe === true, - resolveProviderPreferredProfileId: (context) => - resolveProviderAuthProfileId({ - provider, - config: params.config, - workspaceDir: resolvedWorkspace, - env: process.env, - context, - }), - }); - const providerUsesProfileScopedModelMetadata = providerUsesCredentialScopedModelMetadata({ + const preparedAuthPlan = await prepareEmbeddedRunAuthPlan({ + runParams: params, provider, modelId, - config: params.config, + model, agentDir, workspaceDir: resolvedWorkspace, + requestStreamTransportOverrides, + nativeModelOwned, + authStorage, + modelRegistry, + getAgentHarness: () => agentHarness, + setAgentHarness: (nextHarness) => { + agentHarness = nextHarness; + pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; + }, + getRuntimeModel: () => runtimeModel, + getEffectiveModel: () => effectiveModel, + applyResolvedRuntimeModel, + selectHarnessForPreparedAttempts, + markStage: (stage) => authStages?.mark(stage), }); - const { materialize: materializeAuthPlan, materializeUncached: materializeAuthPlanUncached } = - createPreparedRuntimeModelMaterializer({ - provider, - modelId, - config: params.config, - getModel: () => runtimeModel, - nativeModelOwned, - requestedProfileId: params.authProfileId, - providerUsesProfileScopedModelMetadata, - resolveModel: ({ config, authProfileId, authProfileMode }) => - resolveModelAsync(provider, modelId, agentDir, config, { - authStorage, - modelRegistry, - skipAgentDiscovery: true, - allowBundledStaticCatalogFallback: true, - preferBundledStaticCatalogTransport: true, - workspaceDir: resolvedWorkspace, - authProfileId, - authProfileMode, - }), - }); - let resolvedAuthPreparation = createAuthPreparation(); - let preparedAuthAttempts = resolvedAuthPreparation.attempts; - let activePreparedAuthPlan = resolvedAuthPreparation.plan; - applyResolvedRuntimeModel(await materializeAuthPlan(activePreparedAuthPlan)); - authStages?.mark("prepare-plan"); - - const finalizedHarness = selectHarnessForPreparedAttempts( - effectiveModel, + const { + usesOpenAIAuthRouting, + attemptAuthProfileStore, + lockedProfileId, + preferredProfileId, + providerUsesProfileScopedModelMetadata, + materializeAuthPlan, + materializeAuthPlanUncached, preparedAuthAttempts, - ); - if (finalizedHarness.id !== agentHarness.id) { - agentHarness = finalizedHarness; - pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; - resolvedAuthPreparation = createAuthPreparation(); - preparedAuthAttempts = resolvedAuthPreparation.attempts; - activePreparedAuthPlan = resolvedAuthPreparation.plan; - applyResolvedRuntimeModel(await materializeAuthPlan(activePreparedAuthPlan)); - const confirmedHarness = selectHarnessForPreparedAttempts( - effectiveModel, - preparedAuthAttempts, - ); - if (confirmedHarness.id !== agentHarness.id) { - throw new Error( - `Prepared auth route did not converge on one agent harness for ${provider}/${modelId}.`, - ); - } - } - authStages?.mark("harness"); + } = preparedAuthPlan; + let { activePreparedAuthPlan } = preparedAuthPlan; // A selected plugin harness owns context pressure with its native transcript, // even if it cannot expose manual compaction. Generic recovery is OpenClaw-only. const genericCompactionRecoveryAllowed = !pluginHarnessOwnsTransport; diff --git a/src/agents/embedded-agent-runner/run/auth-plan.ts b/src/agents/embedded-agent-runner/run/auth-plan.ts new file mode 100644 index 000000000000..4bf272e2b822 --- /dev/null +++ b/src/agents/embedded-agent-runner/run/auth-plan.ts @@ -0,0 +1,212 @@ +import { resolveProviderAuthProfileId } from "../../../plugins/provider-runtime.js"; +import type { AuthProfileStore } from "../../auth-profiles.js"; +import { resolveExternalCliAuthOverlayScopeFromSelection } from "../../auth-profiles/external-cli-auth-selection.js"; +import type { AgentHarness } from "../../harness/types.js"; +import { + ensureAuthProfileStore, + ensureAuthProfileStoreWithoutExternalProfiles, +} from "../../model-auth.js"; +import { OPENAI_PROVIDER_ID } from "../../openai-routing.js"; +import { + createPreparedRuntimeModelMaterializer, + providerUsesCredentialScopedModelMetadata, +} from "../../runtime-plan/credential-scoped-model.js"; +import { + prepareAgentRuntimeAuth, + type PreparedAgentRuntimeAuthAttempt, +} from "../../runtime-plan/prepare-auth.js"; +import { resolveModelAsync } from "../model.js"; +import type { RunEmbeddedAgentParams } from "./params.js"; + +type ModelResolution = Awaited>; +type RuntimeModel = NonNullable; + +export async function prepareEmbeddedRunAuthPlan(params: { + runParams: RunEmbeddedAgentParams; + provider: string; + modelId: string; + model: RuntimeModel; + agentDir: string; + workspaceDir: string; + requestStreamTransportOverrides?: "present"; + nativeModelOwned: boolean; + authStorage: ModelResolution["authStorage"]; + modelRegistry: ModelResolution["modelRegistry"]; + getAgentHarness: () => AgentHarness; + setAgentHarness: (harness: AgentHarness) => void; + getRuntimeModel: () => RuntimeModel; + getEffectiveModel: () => RuntimeModel; + applyResolvedRuntimeModel: (model: RuntimeModel) => void; + selectHarnessForPreparedAttempts: ( + model: RuntimeModel, + attempts: readonly PreparedAgentRuntimeAuthAttempt[], + ) => AgentHarness; + markStage?: (stage: string) => void; +}) { + const runParams = params.runParams; + const usesOpenAIAuthRouting = params.provider === OPENAI_PROVIDER_ID; + const initialHarness = params.getAgentHarness(); + const initialPluginHarnessOwnsTransport = initialHarness.id !== "openclaw"; + const openClawNativeCodexResponsesNeedsAuthBootstrap = + !initialPluginHarnessOwnsTransport && + usesOpenAIAuthRouting && + params.getEffectiveModel().api === "openai-chatgpt-responses"; + let externalCliAuthScope = initialPluginHarnessOwnsTransport + ? { ignoreAutoPreferredProfile: false } + : openClawNativeCodexResponsesNeedsAuthBootstrap + ? { + providerIds: [OPENAI_PROVIDER_ID], + ignoreAutoPreferredProfile: false, + } + : resolveExternalCliAuthOverlayScopeFromSelection({ + provider: params.provider, + cfg: runParams.config, + agentId: runParams.agentId, + modelId: params.modelId, + workspaceDir: params.workspaceDir, + userLockedAuthProfileId: + runParams.authProfileIdSource === "user" ? runParams.authProfileId : undefined, + }); + let noExternalAuthStore: AuthProfileStore | undefined; + if (!initialPluginHarnessOwnsTransport && !externalCliAuthScope.providerIds) { + noExternalAuthStore = ensureAuthProfileStoreWithoutExternalProfiles(params.agentDir, { + allowKeychainPrompt: false, + }); + externalCliAuthScope = resolveExternalCliAuthOverlayScopeFromSelection({ + provider: params.provider, + cfg: runParams.config, + agentId: runParams.agentId, + modelId: params.modelId, + workspaceDir: params.workspaceDir, + store: noExternalAuthStore, + userLockedAuthProfileId: + runParams.authProfileIdSource === "user" ? runParams.authProfileId : undefined, + }); + } + params.markStage?.("scope"); + + const attemptAuthProfileStore = usesOpenAIAuthRouting + ? ensureAuthProfileStore(params.agentDir, { + externalCliProviderIds: [OPENAI_PROVIDER_ID], + allowKeychainPrompt: false, + }) + : initialPluginHarnessOwnsTransport + ? ensureAuthProfileStoreWithoutExternalProfiles(params.agentDir, { + allowKeychainPrompt: false, + }) + : externalCliAuthScope.providerIds + ? ensureAuthProfileStore(params.agentDir, { + externalCliProviderIds: externalCliAuthScope.providerIds, + allowKeychainPrompt: false, + }) + : (noExternalAuthStore ?? + ensureAuthProfileStoreWithoutExternalProfiles(params.agentDir, { + allowKeychainPrompt: false, + })); + params.markStage?.("store"); + + const requestedProfileId = runParams.authProfileId?.trim() || undefined; + const lockedProfileId = runParams.authProfileIdSource === "user" ? requestedProfileId : undefined; + const preferredProfileId = + externalCliAuthScope.ignoreAutoPreferredProfile && !lockedProfileId + ? undefined + : requestedProfileId; + const createAuthPreparation = () => { + const harness = params.getAgentHarness(); + return prepareAgentRuntimeAuth({ + provider: params.provider, + modelId: params.modelId, + modelApi: params.model.api, + modelBaseUrl: params.model.baseUrl, + requestTransportOverrides: params.requestStreamTransportOverrides, + config: runParams.config, + env: process.env, + agentDir: params.agentDir, + workspaceDir: params.workspaceDir, + authProfileStore: attemptAuthProfileStore, + sessionAuthProfileId: preferredProfileId, + sessionAuthProfileSource: runParams.authProfileIdSource, + harnessId: harness.id, + harnessRuntime: harness.id, + harnessAuthBootstrap: harness.authBootstrap, + allowHarnessAuthProfileForwarding: true, + allowTransientCooldownProbe: runParams.allowTransientCooldownProbe === true, + resolveProviderPreferredProfileId: (context) => + resolveProviderAuthProfileId({ + provider: params.provider, + config: runParams.config, + workspaceDir: params.workspaceDir, + env: process.env, + context, + }), + }); + }; + const providerUsesProfileScopedModelMetadata = providerUsesCredentialScopedModelMetadata({ + provider: params.provider, + modelId: params.modelId, + config: runParams.config, + agentDir: params.agentDir, + workspaceDir: params.workspaceDir, + }); + const { materialize: materializeAuthPlan, materializeUncached: materializeAuthPlanUncached } = + createPreparedRuntimeModelMaterializer({ + provider: params.provider, + modelId: params.modelId, + config: runParams.config, + getModel: params.getRuntimeModel, + nativeModelOwned: params.nativeModelOwned, + requestedProfileId: runParams.authProfileId, + providerUsesProfileScopedModelMetadata, + resolveModel: ({ config, authProfileId, authProfileMode }) => + resolveModelAsync(params.provider, params.modelId, params.agentDir, config, { + authStorage: params.authStorage, + modelRegistry: params.modelRegistry, + skipAgentDiscovery: true, + allowBundledStaticCatalogFallback: true, + preferBundledStaticCatalogTransport: true, + workspaceDir: params.workspaceDir, + authProfileId, + authProfileMode, + }), + }); + + let resolvedAuthPreparation = createAuthPreparation(); + let preparedAuthAttempts = resolvedAuthPreparation.attempts; + let activePreparedAuthPlan = resolvedAuthPreparation.plan; + params.applyResolvedRuntimeModel(await materializeAuthPlan(activePreparedAuthPlan)); + params.markStage?.("prepare-plan"); + + const finalizedHarness = params.selectHarnessForPreparedAttempts( + params.getEffectiveModel(), + preparedAuthAttempts, + ); + if (finalizedHarness.id !== params.getAgentHarness().id) { + params.setAgentHarness(finalizedHarness); + resolvedAuthPreparation = createAuthPreparation(); + preparedAuthAttempts = resolvedAuthPreparation.attempts; + activePreparedAuthPlan = resolvedAuthPreparation.plan; + params.applyResolvedRuntimeModel(await materializeAuthPlan(activePreparedAuthPlan)); + const confirmedHarness = params.selectHarnessForPreparedAttempts( + params.getEffectiveModel(), + preparedAuthAttempts, + ); + if (confirmedHarness.id !== params.getAgentHarness().id) { + throw new Error( + `Prepared auth route did not converge on one agent harness for ${params.provider}/${params.modelId}.`, + ); + } + } + params.markStage?.("harness"); + + return { + usesOpenAIAuthRouting, + attemptAuthProfileStore, + lockedProfileId, + preferredProfileId, + providerUsesProfileScopedModelMetadata, + materializeAuthPlan, + materializeAuthPlanUncached, + preparedAuthAttempts, + activePreparedAuthPlan, + }; +} diff --git a/src/agents/embedded-agent-runner/run/model-harness.ts b/src/agents/embedded-agent-runner/run/model-harness.ts new file mode 100644 index 000000000000..f3ce5808e201 --- /dev/null +++ b/src/agents/embedded-agent-runner/run/model-harness.ts @@ -0,0 +1,140 @@ +import type { Model } from "../../../llm/types.js"; +import { + selectAgentHarness, + selectAgentHarnessForPreparedModelProviders, + type AgentHarnessPreparedModelProvider, +} from "../../harness/selection.js"; +import { + resolveAgentHarnessPreparedAuthSupport, + resolveAgentHarnessPreparedRouteSupport, +} from "../../harness/support.js"; +import type { AgentHarness } from "../../harness/types.js"; +import { resolveContextConfigProviderForRuntime } from "../../openai-routing.js"; +import type { PreparedAgentRuntimeAuthAttempt } from "../../runtime-plan/prepare-auth.js"; +import type { AgentRuntimeAuthPlan } from "../../runtime-plan/types.js"; +import type { RunEmbeddedAgentParams } from "./params.js"; +import { resolveEmbeddedRuntimeModelPolicy } from "./setup.js"; + +type HarnessSelectionContext = { + runParams: RunEmbeddedAgentParams; + provider: string; + modelId: string; + requestStreamTransportOverrides?: "present"; + nativeModelOwnedHarnessId?: string; +}; + +export function resolveEmbeddedRunEffectiveModel( + params: HarnessSelectionContext & { + modelConfigProvider: string; + agentHarnessId: string; + runtimeModel: Model; + nativeModelOwned: boolean; + }, +) { + return resolveEmbeddedRuntimeModelPolicy({ + cfg: params.runParams.config, + provider: params.provider, + contextConfigProvider: resolveContextConfigProviderForRuntime({ + provider: params.modelConfigProvider, + runtimeId: params.agentHarnessId, + config: params.runParams.config, + }), + modelId: params.modelId, + runtimeModel: params.runtimeModel, + nativeModelOwned: params.nativeModelOwned, + }); +} + +function buildHarnessModelProvider( + params: HarnessSelectionContext & { + model: Model; + plan?: AgentRuntimeAuthPlan; + preparedAuthAttempt?: PreparedAgentRuntimeAuthAttempt; + }, +): AgentHarnessPreparedModelProvider { + const route = params.plan?.modelRoute; + const routeSupport = resolveAgentHarnessPreparedRouteSupport(params.plan); + const requestTransportOverrides = + params.requestStreamTransportOverrides ?? routeSupport.requestTransportOverrides; + return { + api: route?.api ?? params.model.api, + baseUrl: route?.baseUrl ?? params.model.baseUrl, + ...(requestTransportOverrides ? { requestTransportOverrides } : {}), + ...(routeSupport.runtimePolicy ? { runtimePolicy: routeSupport.runtimePolicy } : {}), + ...(params.plan + ? { + preparedAuth: resolveAgentHarnessPreparedAuthSupport({ + plan: params.plan, + ...(params.preparedAuthAttempt?.kind === "profile" || + params.preparedAuthAttempt?.kind === "direct" + ? { source: params.preparedAuthAttempt.kind } + : {}), + }), + } + : {}), + }; +} + +function assertPinnedHarness( + nativeModelOwnedHarnessId: string | undefined, + selected: AgentHarness, + subject: string, +): void { + if (nativeModelOwnedHarnessId && selected.id !== nativeModelOwnedHarnessId) { + throw new Error( + `${subject} changed the session-pinned agent harness from "${nativeModelOwnedHarnessId}" to "${selected.id}".`, + ); + } +} + +export function selectEmbeddedRunHarness( + params: HarnessSelectionContext & { + model: Model; + plan?: AgentRuntimeAuthPlan; + preparedAuthAttempt?: PreparedAgentRuntimeAuthAttempt; + }, +): AgentHarness { + const selected = selectAgentHarness({ + provider: params.provider, + modelId: params.modelId, + modelProvider: buildHarnessModelProvider(params), + config: params.runParams.config, + agentId: params.runParams.agentId, + sessionKey: params.runParams.sessionKey, + agentHarnessId: params.runParams.agentHarnessId, + agentHarnessRuntimeOverride: params.runParams.agentHarnessRuntimeOverride, + }); + assertPinnedHarness(params.nativeModelOwnedHarnessId, selected, "Prepared model route"); + return selected; +} + +export function selectEmbeddedRunHarnessForPreparedAttempts( + params: HarnessSelectionContext & { + model: Model; + attempts: readonly PreparedAgentRuntimeAuthAttempt[]; + }, +): AgentHarness { + const selected = selectAgentHarnessForPreparedModelProviders({ + provider: params.provider, + modelId: params.modelId, + modelProviders: params.attempts.map((attempt) => { + const route = attempt.plan.modelRoute; + const model = route + ? { ...params.model, api: route.api, baseUrl: route.baseUrl } + : params.model; + return buildHarnessModelProvider({ + ...params, + model, + plan: attempt.plan, + preparedAuthAttempt: attempt, + }); + }), + config: params.runParams.config, + agentId: params.runParams.agentId, + sessionKey: params.runParams.sessionKey, + agentHarnessId: params.runParams.agentHarnessId, + agentHarnessRuntimeOverride: params.runParams.agentHarnessRuntimeOverride, + }); + assertPinnedHarness(params.nativeModelOwnedHarnessId, selected, "Prepared auth routes"); + return selected; +} diff --git a/src/agents/embedded-agent-runner/run/model-setup.ts b/src/agents/embedded-agent-runner/run/model-setup.ts new file mode 100644 index 000000000000..9b3a0485b8a5 --- /dev/null +++ b/src/agents/embedded-agent-runner/run/model-setup.ts @@ -0,0 +1,203 @@ +import { FailoverError } from "../../failover-error.js"; +import { ensureSelectedAgentHarnessPlugin } from "../../harness/runtime-plugin.js"; +import { selectAgentHarness } from "../../harness/selection.js"; +import { ensureOpenClawModelsJson } from "../../models-config.js"; +import { resolveSelectedOpenAIRuntimeProvider } from "../../openai-routing.js"; +import { createEmptyAgentDiscoveryStores, resolveModelAsync } from "../model.js"; +import type { RunEmbeddedAgentParams } from "./params.js"; +import { resolveRequestStreamTransportOverrides } from "./runtime-resolution.js"; +import { + buildBeforeModelResolveAttachments, + createNativeModelOwnedRuntimeModel, + resolveHookModelSelection, + resolveNativeModelOwnedHarnessId, +} from "./setup.js"; + +export async function resolveEmbeddedRunModelSetup(params: { + runParams: RunEmbeddedAgentParams; + provider: string; + modelId: string; + agentDir: string; + workspaceDir: string; + globalLane: string; + hookRunner: Parameters[0]["hookRunner"]; + hookContext: Parameters[0]["hookContext"]; + onHooksResolved: () => void; +}) { + const runParams = params.runParams; + const hookSelection = await resolveHookModelSelection({ + prompt: runParams.prompt, + attachments: buildBeforeModelResolveAttachments(runParams.images), + provider: params.provider, + modelId: params.modelId, + modelSelectionLocked: runParams.modelSelectionLocked, + hookRunner: params.hookRunner, + hookContext: params.hookContext, + }); + const modelSelectionChangedByHook = + hookSelection.provider !== params.provider || hookSelection.modelId !== params.modelId; + let provider = hookSelection.provider; + const modelId = hookSelection.modelId; + const requestedModelId = modelId; + const requestStreamTransportOverrides = resolveRequestStreamTransportOverrides( + runParams.streamParams, + ); + params.onHooksResolved(); + + await ensureSelectedAgentHarnessPlugin({ + provider, + modelId, + config: runParams.config, + agentId: runParams.agentId, + sessionKey: runParams.sessionKey, + agentHarnessId: runParams.agentHarnessId, + agentHarnessRuntimeOverride: runParams.agentHarnessRuntimeOverride, + requestTransportOverrides: requestStreamTransportOverrides, + workspaceDir: params.workspaceDir, + }); + const agentHarness = selectAgentHarness({ + provider, + modelId, + ...(requestStreamTransportOverrides + ? { + modelProvider: { + requestTransportOverrides: requestStreamTransportOverrides, + }, + } + : {}), + config: runParams.config, + agentId: runParams.agentId, + sessionKey: runParams.sessionKey, + agentHarnessId: runParams.agentHarnessId, + agentHarnessRuntimeOverride: runParams.agentHarnessRuntimeOverride, + }); + const pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; + const expectedHarnessArtifact = runParams.expectedAgentHarnessRuntimeArtifact; + if (expectedHarnessArtifact && expectedHarnessArtifact.harnessId !== agentHarness.id) { + throw new Error( + `Verified inference requires agent harness ${expectedHarnessArtifact.harnessId}, but ${agentHarness.id} was selected.`, + ); + } + if (expectedHarnessArtifact && !agentHarness.runtimeArtifact) { + throw new Error( + `Agent harness ${agentHarness.id} cannot attest the verified inference runtime artifact.`, + ); + } + + const nativeModelOwnedHarnessId = resolveNativeModelOwnedHarnessId({ + agentHarnessId: runParams.agentHarnessId, + modelSelectionLocked: runParams.modelSelectionLocked, + selectedHarnessId: agentHarness.id, + }); + const nativeModelOwned = nativeModelOwnedHarnessId !== undefined; + const modelConfigProvider = provider; + let resolvedModelProvider = provider; + let firstModelResolution: Awaited> | undefined; + let modelResolution: Awaited> | undefined; + if (nativeModelOwned) { + modelResolution = { + model: createNativeModelOwnedRuntimeModel({ provider, modelId }), + ...createEmptyAgentDiscoveryStores(), + }; + } else { + const selectedRuntimeProvider = resolveSelectedOpenAIRuntimeProvider({ + provider, + harnessRuntime: agentHarness.id, + agentHarnessId: agentHarness.id, + authProfileProvider: runParams.authProfileId?.split(":", 1)[0], + authProfileId: runParams.authProfileId, + config: runParams.config, + workspaceDir: params.workspaceDir, + }); + const modelResolutionProviders = + selectedRuntimeProvider !== provider ? [selectedRuntimeProvider, provider] : [provider]; + for (const candidateProvider of modelResolutionProviders) { + const candidateResolution = await resolveModelAsync( + candidateProvider, + modelId, + params.agentDir, + runParams.config, + { + // Dynamic hooks can resolve an explicit model without generating models.json first. + skipAgentDiscovery: true, + allowBundledStaticCatalogFallback: pluginHarnessOwnsTransport, + preferBundledStaticCatalogTransport: pluginHarnessOwnsTransport, + workspaceDir: params.workspaceDir, + authProfileId: runParams.authProfileId, + }, + ); + firstModelResolution ??= candidateResolution; + if (candidateResolution.model) { + resolvedModelProvider = candidateProvider; + modelResolution = candidateResolution; + break; + } + } + if (!modelResolution && pluginHarnessOwnsTransport) { + modelResolution = firstModelResolution; + } + if (!modelResolution) { + await ensureOpenClawModelsJson(runParams.config, params.agentDir, { + workspaceDir: params.workspaceDir, + }); + for (const candidateProvider of modelResolutionProviders) { + const candidateResolution = await resolveModelAsync( + candidateProvider, + modelId, + params.agentDir, + runParams.config, + { + workspaceDir: params.workspaceDir, + authProfileId: runParams.authProfileId, + allowBundledStaticCatalogFallback: true, + }, + ); + firstModelResolution ??= candidateResolution; + if (candidateResolution.model) { + resolvedModelProvider = candidateProvider; + modelResolution = candidateResolution; + break; + } + } + } + modelResolution ??= firstModelResolution; + } + if (!modelResolution) { + throw new FailoverError(`Unknown model: ${provider}/${modelId}`, { + reason: "model_not_found", + provider, + model: modelId, + sessionId: runParams.sessionId, + lane: params.globalLane, + }); + } + provider = resolvedModelProvider; + const { model, error, authStorage, modelRegistry } = modelResolution; + if (!model) { + throw new FailoverError(error ?? `Unknown model: ${provider}/${modelId}`, { + reason: "model_not_found", + provider, + model: modelId, + sessionId: runParams.sessionId, + lane: params.globalLane, + }); + } + + return { + provider, + modelId, + requestedModelId, + modelSelectionChangedByHook, + beforeAgentStartResult: hookSelection.beforeAgentStartResult, + requestStreamTransportOverrides, + expectedHarnessArtifact, + agentHarness, + pluginHarnessOwnsTransport, + nativeModelOwnedHarnessId, + nativeModelOwned, + modelConfigProvider, + model, + authStorage, + modelRegistry, + }; +}