openclaw / src /agents /command /prepare.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw History Blame Contribute Delete
18.2 kB
import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { resolveSessionStableReplyMode } from "../../auto-reply/reply/session-stable-reply-mode.js";
import { isSyntheticSourceReplyTurn } from "../../auto-reply/reply/source-reply-delivery-mode.js";
import {
formatThinkingLevels,
normalizeThinkLevel,
normalizeVerboseLevel,
} from "../../auto-reply/thinking.js";
import { formatCliCommand } from "../../cli/command-format.js";
import type { InternalSessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { resolveAgentExplicitRecipientSession } from "../../infra/outbound/agent-delivery.js";
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
import {
classifySessionKeyShape,
isUnscopedSessionKeySentinel,
normalizeAgentId,
resolveAgentIdFromSessionKey,
} from "../../routing/session-key.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
AGENT_HARNESS_MODEL_RUN_FORBIDDEN_MESSAGE,
resolveAgentHarnessSessionContextError,
} from "../../sessions/agent-harness-session-key.js";
import {
assertAgentDatabaseAdmitted,
evaluateAgentDatabaseAdmissions,
hasAgentDatabaseAdmissions,
recordAgentDatabaseAdmissions,
} from "../../state/agent-database-admission.js";
import { resolveUserPath } from "../../utils.js";
import { isDeliverableMessageChannel, resolveMessageChannel } from "../../utils/message-channel.js";
import { resolveAgentRuntimeConfig } from "../agent-runtime-config.js";
import { resolveAgentRunCwd } from "../agent-scope-config.js";
import {
listAgentIds,
resolveAgentDir,
resolveSessionAgentId,
resolveAgentWorkspaceDir,
} from "../agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import {
resolveAcpPromptBody,
prependInternalEventContext,
resolveInternalEventTranscriptBody,
} from "../internal-events.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import type { ModelManifestNormalizationContext } from "../model-ref-shared.js";
import { buildConfiguredModelCatalog, resolveConfiguredModelRef } from "../model-selection.js";
import type { PreparedModelRuntimePluginGeneration } from "../prepared-model-runtime.types.js";
import { normalizeSpawnedRunMetadata } from "../spawned-context.js";
import { resolveEffectiveAgentRuntime } from "../thinking-runtime.js";
import { resolveAgentTimeoutMs } from "../timeout.js";
import { ensureAgentWorkspace } from "../workspace.js";
import { acquireWorktreeRunLease, resolveWorktreeIdForPath } from "../worktrees/run-lease.js";
import { resolveExplicitAgentCommandSessionKey } from "./explicit-session-key.js";
import { loadAcpManagerRuntime } from "./runtime-loaders.js";
import { resolveSession } from "./session.js";
import type { AgentCommandOpts } from "./types.js";
const OVERRIDE_VALUE_MAX_LENGTH = 256;
function containsControlCharacters(value: string): boolean {
for (const char of value) {
const code = char.codePointAt(0);
if (code === undefined) {
continue;
}
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
return true;
}
}
return false;
}
export function normalizeExplicitOverrideInput(raw: string, kind: "provider" | "model"): string {
const trimmed = raw.trim();
const label = kind === "provider" ? "Provider" : "Model";
if (!trimmed) {
throw new Error(`${label} override must be non-empty.`);
}
if (trimmed.length > OVERRIDE_VALUE_MAX_LENGTH) {
throw new Error(`${label} override exceeds ${String(OVERRIDE_VALUE_MAX_LENGTH)} characters.`);
}
if (containsControlCharacters(trimmed)) {
throw new Error(`${label} override contains invalid control characters.`);
}
return trimmed;
}
export type PreparedAgentCommandRuntimeContext = Readonly<{
config: OpenClawConfig;
pluginGeneration: PreparedModelRuntimePluginGeneration;
}>;
export async function prepareAgentCommandExecution(
opts: AgentCommandOpts,
runtime: RuntimeEnv,
runtimeContext?: PreparedAgentCommandRuntimeContext,
) {
const isRawModelRun = opts.modelRun === true || opts.promptMode === "none";
const message = opts.message ?? "";
if (!message.trim()) {
throw new Error("Message (--message) is required");
}
const rawExplicitSessionKey = opts.sessionKey?.trim();
const requestedSessionId = opts.sessionId?.trim() || undefined;
const rawTo = opts.to?.trim();
const toSessionKey =
!rawExplicitSessionKey && !requestedSessionId && classifySessionKeyShape(rawTo) === "agent"
? rawTo
: undefined;
const recipientChannel = resolveMessageChannel(opts.channel);
const shouldResolveExplicitRecipientSession = Boolean(
!rawExplicitSessionKey &&
!requestedSessionId &&
!toSessionKey &&
opts.agentId?.trim() &&
recipientChannel &&
isDeliverableMessageChannel(recipientChannel) &&
rawTo,
);
if (!opts.to && !requestedSessionId && !rawExplicitSessionKey && !opts.agentId) {
throw new Error(
"Pass --to <E.164>, --session-key, --session-id, or --agent to choose a session",
);
}
const cfg = await resolveAgentRuntimeConfig(runtime, {
runtimeTargetsChannelSecrets: opts.deliver === true,
runtimeChannelSecretScope:
opts.deliver !== true && shouldResolveExplicitRecipientSession && recipientChannel
? { channel: recipientChannel, accountId: opts.accountId }
: undefined,
});
const normalizedSpawned = normalizeSpawnedRunMetadata({
spawnedBy: opts.spawnedBy,
groupId: opts.groupId,
groupChannel: opts.groupChannel,
groupSpace: opts.groupSpace,
workspaceDir: opts.workspaceDir,
});
const agentIdOverrideRaw = opts.agentId?.trim();
const agentIdOverride = agentIdOverrideRaw ? normalizeAgentId(agentIdOverrideRaw) : undefined;
if (agentIdOverride) {
const knownAgents = listAgentIds(cfg);
if (!knownAgents.includes(agentIdOverride)) {
throw new Error(
`Unknown agent id "${agentIdOverrideRaw}". Use "${formatCliCommand("openclaw agents list")}" to see configured agents.`,
);
}
}
const shouldScopeDefaultAgentKey = Boolean(
rawExplicitSessionKey &&
!agentIdOverride &&
classifySessionKeyShape(rawExplicitSessionKey) === "legacy_or_alias" &&
!isUnscopedSessionKeySentinel(rawExplicitSessionKey),
);
const explicitSessionKey =
toSessionKey ??
resolveExplicitAgentCommandSessionKey({
rawExplicitSessionKey,
agentIdOverride,
shouldScopeDefaultAgentKey,
cfg,
});
if (explicitSessionKey && classifySessionKeyShape(explicitSessionKey) === "malformed_agent") {
throw new Error(
`Invalid --session-key "${explicitSessionKey}". Agent-prefixed session keys must use agent:<agent-id>:<session-key>.`,
);
}
if (
agentIdOverride &&
explicitSessionKey &&
classifySessionKeyShape(explicitSessionKey) === "agent"
) {
const sessionAgentId = resolveAgentIdFromSessionKey(explicitSessionKey);
if (sessionAgentId !== agentIdOverride) {
throw new Error(
`Agent id "${agentIdOverrideRaw}" does not match session key agent "${sessionAgentId}".`,
);
}
}
if (agentIdOverride || explicitSessionKey) {
if (!hasAgentDatabaseAdmissions()) {
recordAgentDatabaseAdmissions(await evaluateAgentDatabaseAdmissions(cfg));
}
assertAgentDatabaseAdmitted(
agentIdOverride ?? resolveSessionAgentId({ sessionKey: explicitSessionKey, config: cfg }),
);
}
const agentCfg = cfg.agents?.defaults;
const verboseOverride = normalizeVerboseLevel(opts.verbose);
if (opts.verbose && !verboseOverride) {
throw new Error('Invalid verbose level. Use "on", "full", or "off".');
}
const laneRaw = normalizeOptionalString(opts.lane) ?? "";
const subagentLane: string = AGENT_LANE_SUBAGENT;
const isSubagentLane = laneRaw === subagentLane;
const hasExplicitTimeoutOption = opts.timeout !== undefined;
const timeoutSecondsRaw = hasExplicitTimeoutOption
? (parseStrictNonNegativeInteger(opts.timeout) ?? Number.NaN)
: isSubagentLane
? 0
: undefined;
if (
timeoutSecondsRaw !== undefined &&
(Number.isNaN(timeoutSecondsRaw) || timeoutSecondsRaw < 0)
) {
throw new Error("--timeout must be a non-negative integer (seconds; 0 means no timeout)");
}
const timeoutMs = resolveAgentTimeoutMs({ cfg, overrideSeconds: timeoutSecondsRaw });
const runTimeoutOverrideMs = hasExplicitTimeoutOption ? timeoutMs : undefined;
const selectedCommandOpts = toSessionKey
? { ...opts, to: undefined, sessionKey: explicitSessionKey }
: opts;
const explicitRecipientSession =
shouldResolveExplicitRecipientSession && agentIdOverride && recipientChannel && rawTo
? await resolveAgentExplicitRecipientSession({
cfg,
agentId: agentIdOverride,
channel: recipientChannel,
to: rawTo,
accountId: selectedCommandOpts.accountId,
threadId: selectedCommandOpts.threadId,
})
: undefined;
if (explicitRecipientSession?.error) {
throw explicitRecipientSession.error;
}
let commandOpts: AgentCommandOpts = explicitRecipientSession?.sessionKey
? {
...selectedCommandOpts,
channel: explicitRecipientSession.channel,
to: explicitRecipientSession.to,
accountId: explicitRecipientSession.accountId,
threadId: explicitRecipientSession.threadId,
}
: selectedCommandOpts;
const sessionResolution = resolveSession({
cfg,
to: commandOpts.to,
sessionId: commandOpts.sessionId,
sessionKey: explicitSessionKey ?? explicitRecipientSession?.sessionKey,
agentId: agentIdOverride,
});
const {
sessionId,
sessionKey,
sessionEntry: sessionEntryRaw,
storePath,
isNewSession,
previousSessionId,
persistedThinking,
persistedVerbose,
} = sessionResolution;
const harnessSessionError = sessionKey
? resolveAgentHarnessSessionContextError(sessionKey, sessionEntryRaw)
: undefined;
if (harnessSessionError) {
throw new Error(harnessSessionError);
}
const isOneShotModelRun = opts.modelRun === true || opts.promptMode === "none";
if (isOneShotModelRun && sessionKey && sessionEntryRaw?.modelSelectionLocked === true) {
throw new Error(AGENT_HARNESS_MODEL_RUN_FORBIDDEN_MESSAGE);
}
const sessionStore: Record<string, InternalSessionEntry> =
sessionKey && sessionEntryRaw ? { [sessionKey]: sessionEntryRaw } : {};
const sessionAgentId =
agentIdOverride ??
resolveSessionAgentId({ sessionKey: sessionKey ?? explicitSessionKey, config: cfg });
assertAgentDatabaseAdmitted(sessionAgentId);
const outboundSession = buildOutboundSessionContext({
cfg,
agentId: sessionAgentId,
sessionKey,
});
const workspaceDirRaw =
normalizedSpawned.workspaceDir ?? resolveAgentWorkspaceDir(cfg, sessionAgentId);
const workspaceDir = resolveUserPath(workspaceDirRaw);
const { getAcpSessionManager } = await loadAcpManagerRuntime();
const acpManager = getAcpSessionManager();
const acpResolution = sessionKey
? acpManager.resolveSession({ cfg, sessionKey, agentId: sessionAgentId })
: null;
// Configured run cwd is a Gateway-local path; ACP-placed sessions ("ready" or
// "stale") execute on their own node with a node-owned execCwd, so the config
// fallback applies only to ordinary sessions and never bridges into a node.
const isAcpPlacedSession = acpResolution !== null && acpResolution.kind !== "none";
const cwd =
normalizeOptionalString(opts.cwd) ??
normalizeOptionalString(sessionEntryRaw?.spawnedCwd) ??
(isAcpPlacedSession ? undefined : resolveAgentRunCwd(cfg, sessionAgentId));
const agentDir = resolveAgentDir(cfg, sessionAgentId);
const pluginsEnabled = cfg.plugins?.enabled !== false;
const preparedMetadataSnapshot = runtimeContext?.pluginGeneration.pluginMetadataSnapshot;
const manifestMetadataSnapshot = pluginsEnabled
? (preparedMetadataSnapshot ??
resolvePluginMetadataSnapshot({ config: cfg, env: process.env, workspaceDir }))
: undefined;
const modelManifestContext = {
manifestPlugins: manifestMetadataSnapshot ?? [],
} satisfies ModelManifestNormalizationContext;
const configuredModel = resolveConfiguredModelRef({
cfg,
agentId: sessionAgentId,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
allowPluginNormalization: pluginsEnabled,
...modelManifestContext,
});
const configuredThinkingCatalog = buildConfiguredModelCatalog({
cfg,
workspaceDir,
...modelManifestContext,
});
const configuredThinkingRuntime = resolveEffectiveAgentRuntime({
cfg,
provider: configuredModel.provider,
modelId: configuredModel.model,
agentId: sessionAgentId,
sessionKey,
sessionEntry: sessionEntryRaw,
});
if (
sessionEntryRaw &&
commandOpts.cliSessionBindingFacts === undefined &&
isSyntheticSourceReplyTurn({
inputProvenance: commandOpts.inputProvenance,
isHeartbeat: commandOpts.bootstrapContextRunKind === "heartbeat",
})
) {
commandOpts = {
...commandOpts,
cliSessionBindingFacts: {
sourceReplyDeliveryMode: resolveSessionStableReplyMode({
cfg,
ctx: { CommandAuthorized: false },
sessionEntry: sessionEntryRaw,
sessionAgentId,
sessionKey,
}),
},
};
}
const thinkingLevelsHint = formatThinkingLevels(
configuredModel.provider,
configuredModel.model,
", ",
configuredThinkingCatalog.length > 0 ? configuredThinkingCatalog : undefined,
configuredThinkingRuntime,
);
const thinkOverride = normalizeThinkLevel(opts.thinking);
const thinkOnce = normalizeThinkLevel(opts.thinkingOnce);
if (opts.thinking && !thinkOverride) {
throw new Error(`Invalid thinking level. Use one of: ${thinkingLevelsHint}.`);
}
if (opts.thinkingOnce && !thinkOnce) {
throw new Error(`Invalid one-shot thinking level. Use one of: ${thinkingLevelsHint}.`);
}
const resolvedCwd = cwd ? resolveUserPath(cwd) : undefined;
const worktreeId = await resolveWorktreeIdForPath({
sessionEntry: sessionEntryRaw,
candidatePaths: [resolvedCwd ?? workspaceDir, workspaceDir],
});
const runLease = worktreeId ? await acquireWorktreeRunLease(worktreeId) : undefined;
try {
const { resolveAcpAgentWorkspaceProvisioningForTurn } =
await import("../acp-workspace-provisioning.js");
const workspaceProvisioning = await resolveAcpAgentWorkspaceProvisioningForTurn({
cfg,
agentId: sessionAgentId,
workspaceDir,
cwd: resolvedCwd,
sessionKey: sessionKey ?? undefined,
sessionEntry: sessionEntryRaw ?? undefined,
});
await ensureAgentWorkspace({
dir: workspaceDirRaw,
ensureBootstrapFiles: !agentCfg?.skipBootstrap,
skipOptionalBootstrapFiles: agentCfg?.skipOptionalBootstrapFiles,
provisioning: workspaceProvisioning,
});
const runId = opts.runId?.trim() || sessionId;
let promptMessage = message;
if (!isRawModelRun && (message.includes("$") || message.trimStart().startsWith("/"))) {
const {
expandExplicitSkillReferences,
hasSkillReferenceCandidate,
listSkillCommandsForWorkspace,
resolveEffectiveAgentSkillFilter,
} = await import("../../skills/discovery/chat-commands.runtime.js");
const hasExplicitSkillCandidate =
message.trimStart().startsWith("/") || hasSkillReferenceCandidate(message);
if (hasExplicitSkillCandidate) {
const skillFilter = resolveEffectiveAgentSkillFilter(cfg, sessionAgentId);
const commandParams = {
workspaceDir,
cfg,
agentId: sessionAgentId,
sessionEntry: sessionEntryRaw,
sessionKey,
...(preparedMetadataSnapshot ? { pluginMetadataSnapshot: preparedMetadataSnapshot } : {}),
...(skillFilter ? { skillFilter } : {}),
};
const skillCommands = listSkillCommandsForWorkspace(commandParams);
const allSkillCommands = skillFilter
? listSkillCommandsForWorkspace({ ...commandParams, includeAllowlistHidden: true })
: skillCommands;
const expansion = expandExplicitSkillReferences({
text: message,
skillCommands,
allSkillCommands,
});
if (expansion.error) {
throw new Error(expansion.error);
}
promptMessage = expansion.body;
}
}
const body =
!isRawModelRun && acpResolution?.kind === "ready"
? resolveAcpPromptBody(promptMessage, opts.internalEvents, opts.inputProvenance)
: prependInternalEventContext(promptMessage, opts.internalEvents, opts.inputProvenance);
const transcriptBody =
opts.transcriptMessage ??
resolveInternalEventTranscriptBody(message, opts.internalEvents, opts.inputProvenance);
const prepared = {
opts: commandOpts,
body,
transcriptBody,
cfg,
configuredThinkingCatalog,
normalizedSpawned,
agentCfg,
thinkOverride,
thinkOnce,
verboseOverride,
timeoutMs,
runTimeoutOverrideMs,
sessionId,
sessionKey,
sessionEntry: sessionEntryRaw,
sessionStore,
storePath,
isNewSession,
previousSessionId,
persistedThinking,
persistedVerbose,
sessionAgentId,
outboundSession,
workspaceDir,
cwd: resolvedCwd,
agentDir,
pluginsEnabled,
manifestMetadataSnapshot,
...(runtimeContext ? { commandRuntimeContext: runtimeContext } : {}),
modelManifestContext,
runId,
isSubagentLane,
acpManager,
acpResolution,
runLease,
};
return prepared;
} catch (error) {
await runLease?.release();
throw error;
}
}
export type PreparedAgentCommandExecution = Awaited<
ReturnType<typeof prepareAgentCommandExecution>
>;