UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

506 lines (505 loc) 18.5 kB
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { l as readChannelContextAdmissionEvidence } from "./admission-evidence-BW_2Woj4.js"; import { xt as updateSessionEntry } from "./session-accessor-YsytfDtG.js"; import { a as normalizeChannelId, t as getChannelPlugin } from "./registry-Cz6cv4VC.js"; import "./plugins-Cozj1Enf.js"; import { i as resolveAcpThreadSessionDetailLines, n as resolveAcpSessionCwd } from "./session-identifiers-B5CDFQVW.js"; import { n as resolveSessionStorePathForAcp } from "./session-meta-store-BkOt1zC9.js"; import { s as resolveAcpSessionResolutionError } from "./manager.utils-sCXNDTX4.js"; import { t as getAcpSessionManager } from "./manager-Bbp2Ogkb.js"; import { t as resolveChannelDefaultBindingPlacement } from "./conversation-resolution-BCtsaj9X.js"; import { n as normalizeConversationRef } from "./session-binding-normalization-DA3Q-Bzn.js"; import { t as getSessionBindingService } from "./session-binding-service-DTXs8JDw.js"; import "./session-meta-dzG4VkYo.js"; import { a as prepareAgentRunAdmission, r as createOperationalRunInstanceRef, t as closeAdmittedRunDelegatedAuthority } from "./admitted-run-context-CHY5SdVF.js"; import { i as resolveAcpDispatchPolicyMessage, n as resolveAcpAgentPolicyError, r as resolveAcpDispatchPolicyError, t as isAcpEnabledByPolicy } from "./policy-Dc3ayPTK.js"; import { a as resolveThreadBindingIdleTimeoutMsForChannel, c as resolveThreadBindingSpawnPolicy, n as formatThreadBindingSpawnDisabledError, s as resolveThreadBindingMaxAgeMsForChannel, t as formatThreadBindingDisabledError } from "./thread-bindings-policy-BzrpAQe3.js"; import { i as resolveSpawnedWorkspaceInheritance } from "./spawned-context-Dt3YQx79.js"; import { i as resolveThreadBindingThreadName, r as resolveThreadBindingIntroText } from "./thread-bindings-messages-BnKw5QTY.js"; import { n as commandReply } from "./command-gates-B4tlStPI.js"; import { i as cleanupFailedAcpSpawn, r as resolveRuntimeCwdForAcpSpawn, t as resolveAcpSpawnRuntimePolicyError } from "./acp-spawn-BQ1IfNF1.js"; import { t as consumeChannelRunAdmission } from "./channel-run-admission-CUuWSD0O.js"; import { t as resolveAcpCommandBindingContext } from "./context-BB7shGi1.js"; import { C as withAcpCommandErrorBoundary, S as resolveCommandRequestId, f as collectAcpErrorText, v as parseSpawnInput, y as parseSteerInput } from "./shared-DkkmRMYW.js"; import { t as resolveAcpTargetSessionKey } from "./targets-CwA8rh58.js"; import { randomUUID } from "node:crypto"; //#region src/auto-reply/reply/commands-acp/bindings.ts function resolveAcpBindingLabelNoun(params) { if (params.placement === "child") return "thread"; if (!params.threadId) return "conversation"; return params.conversationId === params.threadId ? "thread" : "conversation"; } async function resolveBoundReplyPayload(params) { const channelId = normalizeChannelId(params.binding.conversation.channel); if (!channelId) return; const buildPayload = getChannelPlugin(channelId)?.conversationBindings?.buildBoundReplyPayload; if (!buildPayload) return; return await buildPayload({ operation: "acp-spawn", placement: params.placement, conversation: params.binding.conversation }) ?? void 0; } function buildSpawnedAcpBindingMetadata(params) { return { threadName: resolveThreadBindingThreadName({ agentId: params.agentId, label: params.label }), agentId: params.agentId, label: params.label, boundBy: params.senderId || "unknown", introText: resolveThreadBindingIntroText({ agentId: params.agentId, label: params.label, idleTimeoutMs: resolveThreadBindingIdleTimeoutMsForChannel({ cfg: params.cfg, channel: params.channel, accountId: params.accountId }), maxAgeMs: resolveThreadBindingMaxAgeMsForChannel({ cfg: params.cfg, channel: params.channel, accountId: params.accountId }), sessionCwd: resolveAcpSessionCwd(params.sessionMeta), sessionDetails: resolveAcpThreadSessionDetailLines({ sessionKey: params.sessionKey, meta: params.sessionMeta }) }) }; } async function bindSpawnedAcpSession(params) { const { commandParams } = params; const currentConversation = params.mode === "conversation"; const bindingContext = resolveAcpCommandBindingContext(commandParams); const { channel, accountId, conversationId, threadId } = bindingContext; if (!channel) return { ok: false, error: `ACP ${currentConversation ? "current-conversation" : "thread"} binding requires a channel context.` }; const policy = resolveThreadBindingSpawnPolicy({ cfg: commandParams.cfg, channel, accountId, kind: "acp" }); if (!policy.enabled) return { ok: false, error: formatThreadBindingDisabledError({ ...policy, kind: "acp" }) }; if (!currentConversation && !policy.spawnEnabled) return { ok: false, error: formatThreadBindingSpawnDisabledError({ ...policy, kind: "acp" }) }; const bindingService = getSessionBindingService(); const capabilities = bindingService.getCapabilities({ channel: policy.channel, accountId: policy.accountId }); const bindingLabel = currentConversation ? "Conversation" : "Thread"; if (!capabilities.adapterAvailable || !capabilities.bindSupported) return { ok: false, error: `${bindingLabel} bindings are unavailable for ${channel}.` }; const defaultPlacement = currentConversation ? "current" : resolveChannelDefaultBindingPlacement(channel) ?? "current"; if (params.mode === "thread-here") { if (!(defaultPlacement === "child" ? threadId : conversationId)) return { ok: false, error: `--thread here requires running /acp spawn inside an active ${channel} thread/conversation.` }; } const placement = currentConversation || threadId ? "current" : defaultPlacement; if (!capabilities.placements.includes(placement)) return { ok: false, error: `${bindingLabel} bindings do not support ${placement} placement for ${channel}.` }; if (!conversationId) return { ok: false, error: currentConversation ? `--bind here requires running /acp spawn inside an active ${channel} conversation.` : `Could not resolve a ${channel} conversation for ACP thread spawn.` }; const senderId = normalizeOptionalString(commandParams.command.senderId) ?? ""; const conversationRef = normalizeConversationRef({ channel: policy.channel, accountId: policy.accountId, conversationId, parentConversationId: bindingContext.parentConversationId }); const labelNoun = resolveAcpBindingLabelNoun({ placement, threadId, conversationId }); if (placement === "current") { const existingBinding = bindingService.resolveByConversation(conversationRef); const boundBy = normalizeOptionalString(existingBinding?.metadata?.boundBy) ?? ""; if (existingBinding && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) return { ok: false, error: `Only ${boundBy} can rebind this ${labelNoun}.` }; } try { return { ok: true, bound: { binding: await bindingService.bind({ targetSessionKey: params.sessionKey, targetKind: "session", conversation: conversationRef, placement, metadata: buildSpawnedAcpBindingMetadata({ cfg: commandParams.cfg, channel: policy.channel, accountId: policy.accountId, sessionKey: params.sessionKey, agentId: params.agentId, label: params.label || params.agentId, senderId, sessionMeta: params.sessionMeta }) }), placement, labelNoun } }; } catch (error) { return { ok: false, error: formatErrorMessage(error) || (currentConversation ? `Failed to bind the current ${channel} conversation to the new ACP session.` : `Failed to bind a ${channel} thread/conversation to the new ACP session.`) }; } } //#endregion //#region src/auto-reply/reply/commands-acp/lifecycle.ts async function persistSpawnedSessionLabel(params) { const label = normalizeOptionalString(params.label); if (!label) return; const now = Date.now(); const { storePath, agentId } = resolveSessionStorePathForAcp({ cfg: params.commandParams.cfg, sessionKey: params.sessionKey, agentId: params.agentId }); if (params.commandParams.sessionStore && params.commandParams.storePath === storePath) { const existing = params.commandParams.sessionStore[params.sessionKey]; if (existing) params.commandParams.sessionStore[params.sessionKey] = { ...existing, label, updatedAt: now }; } await updateSessionEntry({ storePath, agentId, sessionKey: params.sessionKey }, () => ({ label, updatedAt: now })); } async function handleAcpSpawnAction(params, restTokens) { if (!isAcpEnabledByPolicy(params.cfg)) return commandReply("ACP is disabled by policy (`acp.enabled=false`)."); const parsed = parseSpawnInput(params, restTokens); if (!parsed.ok) return commandReply(`⚠️ ${parsed.error}`); const spawn = parsed.value; const runtimePolicyError = resolveAcpSpawnRuntimePolicyError({ cfg: params.cfg, requesterAgentId: params.agentId, requesterSessionKey: params.sessionKey }); if (runtimePolicyError) return commandReply(`⚠️ ${runtimePolicyError}`); const agentPolicyError = resolveAcpAgentPolicyError(params.cfg, spawn.agentId); if (agentPolicyError) return commandReply(collectAcpErrorText({ error: agentPolicyError, fallbackCode: "ACP_SESSION_INIT_FAILED", fallbackMessage: "ACP target agent is not allowed by policy." })); const acpManager = getAcpSessionManager(); const sessionKey = `agent:${spawn.agentId}:acp:${randomUUID()}`; const resolvedCwd = resolveSpawnedWorkspaceInheritance({ config: params.cfg, targetAgentId: spawn.agentId, requesterSessionKey: params.sessionKey, explicitWorkspaceDir: spawn.cwd }); let runtimeCwd; try { runtimeCwd = await resolveRuntimeCwdForAcpSpawn({ resolvedCwd, explicitCwd: spawn.cwd }); } catch (error) { return commandReply(collectAcpErrorText({ error, fallbackCode: "ACP_SESSION_INIT_FAILED", fallbackMessage: "Could not resolve ACP session workspace." })); } let initializedBackend; let initializedMeta; let sessionEntry; let closeRuntimeOnFailure; try { const initialized = await acpManager.initializeSession({ cfg: params.cfg, sessionKey, agentId: spawn.agentId, agent: spawn.agentId, mode: spawn.mode, cwd: runtimeCwd }); sessionEntry = initialized.sessionEntry; closeRuntimeOnFailure = initialized.closeRuntimeOnFailure; initializedBackend = initialized.handle.backend || initialized.meta.backend; initializedMeta = initialized.meta; } catch (err) { return commandReply(collectAcpErrorText({ error: err, fallbackCode: "ACP_SESSION_INIT_FAILED", fallbackMessage: "Could not initialize ACP session runtime." })); } let boundSession; if (spawn.bind !== "off" || spawn.thread !== "off") { const result = await bindSpawnedAcpSession({ commandParams: params, sessionKey, agentId: spawn.agentId, label: spawn.label, mode: spawn.bind !== "off" ? "conversation" : spawn.thread === "here" ? "thread-here" : "thread-auto", sessionMeta: initializedMeta }); if (!result.ok) { await cleanupFailedAcpSpawn({ cfg: params.cfg, sessionKey, agentId: spawn.agentId, sessionEntry, deleteTranscript: false, closeRuntimeOnFailure }); return commandReply(`⚠️ ${result.error}`); } boundSession = result.bound; } try { await persistSpawnedSessionLabel({ commandParams: params, sessionKey, agentId: spawn.agentId, label: spawn.label }); } catch (err) { await cleanupFailedAcpSpawn({ cfg: params.cfg, sessionKey, agentId: spawn.agentId, sessionEntry, deleteTranscript: false, closeRuntimeOnFailure }); const message = formatErrorMessage(err); return commandReply(`⚠️ ACP spawn failed: ${message}`); } const parts = [`✅ Spawned ACP session ${sessionKey} (${spawn.mode}, backend ${initializedBackend}).`]; if (boundSession) { const { binding, placement, labelNoun } = boundSession; const boundConversationId = binding.conversation.conversationId.trim(); if (placement === "current") parts.push(`Bound this ${labelNoun} to ${sessionKey}.`); else parts.push(`Created ${labelNoun} ${boundConversationId} and bound it to ${sessionKey}.`); const boundReplyPayload = await resolveBoundReplyPayload({ binding, placement }); if (boundReplyPayload) return { shouldContinue: false, reply: { text: parts.join(" "), ...boundReplyPayload } }; } else parts.push("Session is unbound (use /acp spawn ... --bind here to create a session bound to this conversation)."); const dispatchNote = resolveAcpDispatchPolicyMessage(params.cfg); if (dispatchNote) parts.push(`ℹ️ ${dispatchNote}`); return commandReply(parts.join(" ")); } function resolveAcpSessionForCommandOrStop(params) { const resolved = params.acpManager.resolveSession({ cfg: params.cfg, sessionKey: params.sessionKey, agentId: params.agentId }); const error = resolveAcpSessionResolutionError(resolved); if (error) return commandReply(collectAcpErrorText({ error, fallbackCode: "ACP_SESSION_INIT_FAILED", fallbackMessage: error.message })); return null; } async function resolveAcpTokenTargetSessionKeyOrStop(params) { const token = normalizeOptionalString(params.restTokens.join(" ")); const target = await resolveAcpTargetSessionKey({ commandParams: params.commandParams, token }); if (!target.ok) return commandReply(`⚠️ ${target.error}`); return target; } async function withResolvedAcpSessionTarget(params) { const acpManager = getAcpSessionManager(); const target = await resolveAcpTokenTargetSessionKeyOrStop({ commandParams: params.commandParams, restTokens: params.restTokens }); if (!("sessionKey" in target)) return target; const guardFailure = resolveAcpSessionForCommandOrStop({ acpManager, cfg: params.commandParams.cfg, ...target }); if (guardFailure) return guardFailure; return await params.run({ acpManager, ...target }); } async function handleAcpCancelAction(params, restTokens) { return await withResolvedAcpSessionTarget({ commandParams: params, restTokens, run: async ({ acpManager, sessionKey, agentId }) => await withAcpCommandErrorBoundary({ run: async () => await acpManager.cancelSession({ cfg: params.cfg, sessionKey, agentId, reason: "manual-cancel" }), fallbackCode: "ACP_TURN_FAILED", fallbackMessage: "ACP cancel failed before completion.", onSuccess: () => commandReply(`✅ Cancel requested for ACP session ${sessionKey}.`) }) }); } async function runAcpSteer(params) { const acpManager = getAcpSessionManager(); let output = ""; const channelAdmission = consumeChannelRunAdmission(params.channelAdmissionEvidence); const admittedRunContext = await prepareAgentRunAdmission({ cfg: params.cfg, operationalRunInstance: createOperationalRunInstanceRef(params.requestId), facts: { runId: params.requestId, agentId: params.agentId, ingress: { kind: "acp", boundary: "acp.command.steer", state: channelAdmission.ingressState }, ...channelAdmission.facts }, onAdmitted: channelAdmission.onAdmitted }).admit("acp"); try { await acpManager.runTurn({ admittedRunContext, cfg: params.cfg, sessionKey: params.sessionKey, agentId: params.agentId, provenance: "agent", text: params.instruction, mode: "steer", requestId: params.requestId, onEvent: (event) => { if (event.type !== "text_delta") return; if (event.stream && event.stream !== "output") return; if (event.text) { output += event.text; if (output.length > 800) output = `${truncateUtf16Safe(output, 800)}…`; } } }); } finally { closeAdmittedRunDelegatedAuthority(admittedRunContext); } return output.trim(); } async function handleAcpSteerAction(params, restTokens) { const dispatchPolicyError = resolveAcpDispatchPolicyError(params.cfg); if (dispatchPolicyError) return commandReply(collectAcpErrorText({ error: dispatchPolicyError, fallbackCode: "ACP_DISPATCH_DISABLED", fallbackMessage: dispatchPolicyError.message })); const parsed = parseSteerInput(restTokens); if (!parsed.ok) return commandReply(`⚠️ ${parsed.error}`); const acpManager = getAcpSessionManager(); const target = await resolveAcpTargetSessionKey({ commandParams: params, token: parsed.value.sessionToken }); if (!target.ok) return commandReply(`⚠️ ${target.error}`); const guardFailure = resolveAcpSessionForCommandOrStop({ acpManager, cfg: params.cfg, ...target }); if (guardFailure) return guardFailure; return await withAcpCommandErrorBoundary({ run: async () => await runAcpSteer({ cfg: params.cfg, ...target, instruction: parsed.value.instruction, requestId: `${resolveCommandRequestId(params)}:steer`, channelAdmissionEvidence: readChannelContextAdmissionEvidence(params.rootCtx ?? params.ctx) }), fallbackCode: "ACP_TURN_FAILED", fallbackMessage: "ACP steer failed before completion.", onSuccess: (steerOutput) => { if (!steerOutput) return commandReply(`✅ ACP steer sent to ${target.sessionKey}.`); return commandReply(`✅ ACP steer sent to ${target.sessionKey}.\n${steerOutput}`); } }); } async function handleAcpCloseAction(params, restTokens) { return await withResolvedAcpSessionTarget({ commandParams: params, restTokens, run: async ({ acpManager, sessionKey, agentId }) => { let runtimeNotice; try { const closed = await acpManager.closeSession({ cfg: params.cfg, sessionKey, agentId, reason: "manual-close", allowBackendUnavailable: true, clearMeta: true }); runtimeNotice = closed.runtimeNotice ? ` (${closed.runtimeNotice})` : ""; } catch (error) { return commandReply(collectAcpErrorText({ error, fallbackCode: "ACP_TURN_FAILED", fallbackMessage: "ACP close failed before completion." })); } const removedBindings = await getSessionBindingService().unbind({ targetSessionKey: sessionKey, reason: "manual" }); return commandReply(`✅ Closed ACP session ${sessionKey}${runtimeNotice}. Removed ${removedBindings.length} binding${removedBindings.length === 1 ? "" : "s"}.`); } }); } //#endregion export { handleAcpCancelAction, handleAcpCloseAction, handleAcpSpawnAction, handleAcpSteerAction };