import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId, } from "../../agents/agent-scope.js"; import { resolveModelRefFromString } from "../../agents/model-selection.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js"; import { type ClawdbotConfig, loadConfig } from "../../config/config.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveCommandAuthorization } from "../command-auth.js"; import type { MsgContext } from "../templating.js"; import { SILENT_REPLY_TOKEN } from "../tokens.js"; import { applyMediaUnderstanding } from "../../media-understanding/apply.js"; import { applyLinkUnderstanding } from "../../link-understanding/apply.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { resolveDefaultModel } from "./directive-handling.js"; import { resolveReplyDirectives } from "./get-reply-directives.js"; import { handleInlineActions } from "./get-reply-inline-actions.js"; import { runPreparedReply } from "./get-reply-run.js"; import { finalizeInboundContext } from "./inbound-context.js"; import { initSessionState } from "./session.js"; import { applyResetModelOverride } from "./session-reset-model.js"; import { stageSandboxMedia } from "./stage-sandbox-media.js"; import { createTypingController } from "./typing.js"; export async function getReplyFromConfig( ctx: MsgContext, opts?: GetReplyOptions, configOverride?: ClawdbotConfig, ): Promise { const isFastTestEnv = process.env.CLAWDBOT_TEST_FAST === "1"; const cfg = configOverride ?? loadConfig(); const targetSessionKey = ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined; const agentSessionKey = targetSessionKey || ctx.SessionKey; const agentId = resolveSessionAgentId({ sessionKey: agentSessionKey, config: cfg, }); const agentCfg = cfg.agents?.defaults; const sessionCfg = cfg.session; const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({ cfg, agentId, }); let provider = defaultProvider; let model = defaultModel; if (opts?.isHeartbeat) { const heartbeatRaw = agentCfg?.heartbeat?.model?.trim() ?? ""; const heartbeatRef = heartbeatRaw ? resolveModelRefFromString({ raw: heartbeatRaw, defaultProvider, aliasIndex, }) : null; if (heartbeatRef) { provider = heartbeatRef.ref.provider; model = heartbeatRef.ref.model; } } const workspaceDirRaw = resolveAgentWorkspaceDir(cfg, agentId) ?? DEFAULT_AGENT_WORKSPACE_DIR; const workspace = await ensureAgentWorkspace({ dir: workspaceDirRaw, ensureBootstrapFiles: !agentCfg?.skipBootstrap && !isFastTestEnv, }); const workspaceDir = workspace.dir; const agentDir = resolveAgentDir(cfg, agentId); const timeoutMs = resolveAgentTimeoutMs({ cfg }); const configuredTypingSeconds = agentCfg?.typingIntervalSeconds ?? sessionCfg?.typingIntervalSeconds; const typingIntervalSeconds = typeof configuredTypingSeconds === "number" ? configuredTypingSeconds : 6; const typing = createTypingController({ onReplyStart: opts?.onReplyStart, typingIntervalSeconds, silentToken: SILENT_REPLY_TOKEN, log: defaultRuntime.log, }); opts?.onTypingController?.(typing); const finalized = finalizeInboundContext(ctx); if (!isFastTestEnv) { await applyMediaUnderstanding({ ctx: finalized, cfg, agentDir, activeModel: { provider, model }, }); await applyLinkUnderstanding({ ctx: finalized, cfg, }); } const commandAuthorized = finalized.CommandAuthorized; resolveCommandAuthorization({ ctx: finalized, cfg, commandAuthorized, }); const sessionState = await initSessionState({ ctx: finalized, cfg, commandAuthorized, }); let { sessionCtx, sessionEntry, previousSessionEntry, sessionStore, sessionKey, sessionId, isNewSession, resetTriggered, systemSent, abortedLastRun, storePath, sessionScope, groupResolution, isGroup, triggerBodyNormalized, bodyStripped, } = sessionState; await applyResetModelOverride({ cfg, resetTriggered, bodyStripped, sessionCtx, ctx: finalized, sessionEntry, sessionStore, sessionKey, storePath, defaultProvider, defaultModel, aliasIndex, }); const directiveResult = await resolveReplyDirectives({ ctx: finalized, cfg, agentId, agentDir, workspaceDir, agentCfg, sessionCtx, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, groupResolution, isGroup, triggerBodyNormalized, commandAuthorized, defaultProvider, defaultModel, aliasIndex, provider, model, typing, opts, skillFilter: opts?.skillFilter, }); if (directiveResult.kind === "reply") { return directiveResult.reply; } let { commandSource, command, allowTextCommands, skillCommands, directives, cleanedBody, elevatedEnabled, elevatedAllowed, elevatedFailures, defaultActivation, resolvedThinkLevel, resolvedVerboseLevel, resolvedReasoningLevel, resolvedElevatedLevel, execOverrides, blockStreamingEnabled, blockReplyChunking, resolvedBlockStreamingBreak, provider: resolvedProvider, model: resolvedModel, modelState, contextTokens, inlineStatusRequested, directiveAck, perMessageQueueMode, perMessageQueueOptions, } = directiveResult.result; provider = resolvedProvider; model = resolvedModel; const inlineActionResult = await handleInlineActions({ ctx, sessionCtx, cfg, agentId, agentDir, sessionEntry, previousSessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, isGroup, opts, typing, allowTextCommands, inlineStatusRequested, command, skillCommands, directives, cleanedBody, elevatedEnabled, elevatedAllowed, elevatedFailures, defaultActivation: () => defaultActivation, resolvedThinkLevel, resolvedVerboseLevel, resolvedReasoningLevel, resolvedElevatedLevel, resolveDefaultThinkingLevel: modelState.resolveDefaultThinkingLevel, provider, model, contextTokens, directiveAck, abortedLastRun, skillFilter: opts?.skillFilter, }); if (inlineActionResult.kind === "reply") { return inlineActionResult.reply; } directives = inlineActionResult.directives; abortedLastRun = inlineActionResult.abortedLastRun ?? abortedLastRun; await stageSandboxMedia({ ctx, sessionCtx, cfg, sessionKey, workspaceDir, }); return runPreparedReply({ ctx, sessionCtx, cfg, agentId, agentDir, agentCfg, sessionCfg, commandAuthorized, command, commandSource, allowTextCommands, directives, defaultActivation, resolvedThinkLevel, resolvedVerboseLevel, resolvedReasoningLevel, resolvedElevatedLevel, execOverrides, elevatedEnabled, elevatedAllowed, blockStreamingEnabled, blockReplyChunking, resolvedBlockStreamingBreak, modelState, provider, model, perMessageQueueMode, perMessageQueueOptions, typing, opts, defaultProvider, defaultModel, timeoutMs, isNewSession, resetTriggered, systemSent, sessionEntry, sessionStore, sessionKey, sessionId, storePath, workspaceDir, abortedLastRun, }); }