UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

429 lines (428 loc) • 17.2 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { t as normalizeAnyChannelId } from "./registry-normalize-DKVgg-js.mjs"; import { c as readChannelContextAdmissionEvidence } from "./admission-evidence-CcY0jAvs.mjs"; import { a as closeAdmittedRunDelegatedAuthority } from "./admitted-run-context-D0yzk2io.mjs"; import { t as getChannelPlugin } from "./registry-Bcrk1FVy.mjs"; import "./plugins-BXkKReMp.mjs"; import "./session-accessor-C3ljJ974.mjs"; import { l as updateSessionEntry } from "./session-accessor.entry-mutation-telsi3Ig.mjs"; import { i as resolveAcpThreadSessionDetailLines, n as resolveAcpSessionCwd } from "./session-identifiers-2CkhpQUq.mjs"; import { n as resolveSessionStorePathForAcp } from "./session-meta-store-CUp3yxTU.mjs"; import { u as resolveAcpSessionResolutionError } from "./manager.utils-B9Kz1tCD.mjs"; import { n as getAcpSessionManager } from "./manager-sn_kcSXp.mjs"; import "./session-meta-CSN8P5u2.mjs"; import { v as normalizeConversationRef } from "./current-conversation-bindings.kernel-DWqaaBL6.mjs"; import { C as expectedCurrentSessionBinding, D as resolveChannelDefaultBindingPlacement, t as getSessionBindingService } from "./session-binding-service-wvWqRl2A.mjs"; import { n as commandReply } from "./command-gates-DXl2hvMx.mjs"; import { r as getGatewayLocalUserIngress } from "./local-user-ingress-PBPakyQk.mjs"; import { i as resolveAcpDispatchPolicyMessage, n as resolveAcpAgentPolicyError, r as resolveAcpDispatchPolicyError, t as isAcpEnabledByPolicy } from "./policy-B6crihax.mjs"; import { a as resolveThreadBindingIdleTimeoutMsForChannel, c as resolveThreadBindingSpawnPolicy, n as formatThreadBindingSpawnDisabledError, s as resolveThreadBindingMaxAgeMsForChannel, t as formatThreadBindingDisabledError } from "./thread-bindings-policy-Dn2kJAuU.mjs"; import { i as resolveSpawnedWorkspaceInheritance } from "./spawned-context-Dw3ZU-zS.mjs"; import { i as resolveThreadBindingThreadName, r as resolveThreadBindingIntroText } from "./thread-bindings-messages-BiGDDOIC.mjs"; import { i as resolveRuntimeCwdForAcpSpawn, o as resolveAcpSpawnRuntimePolicyError, s as cleanupFailedAcpSpawn } from "./acp-spawn-runtime-DcfazyEf.mjs"; import { t as prepareChannelRunAdmission } from "./channel-run-admission-Bz64bSPr.mjs"; import { r as resolveAcpCommandBindingContext, t as resolveAcpTargetSessionKey } from "./targets-Cn3mUj24.mjs"; import { _ as parseSpawnInput, b as resolveCommandRequestId, f as acpCommandErrorReply, v as parseSteerInput, x as withAcpCommandErrorBoundary } from "./shared-CsI73TMh.mjs"; import { randomUUID } from "node:crypto"; //#region src/auto-reply/reply/commands-acp/bindings.ts async function resolveBoundReplyPayload(params) { const channelId = normalizeAnyChannelId(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; } 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 = placement === "child" || threadId && conversationId === threadId ? "thread" : "conversation"; let existingBinding; if (placement === "current") { existingBinding = await bindingService.resolveByConversationAsync(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 { commandParams.command.assertOwnerCurrent?.(); const label = params.label || params.agentId; const lifecycleScope = { cfg: commandParams.cfg, channel: policy.channel, accountId: policy.accountId }; const bindingInput = { [expectedCurrentSessionBinding]: existingBinding, targetSessionKey: params.sessionKey, targetKind: "session", conversation: conversationRef, placement, assertCurrent: commandParams.command.assertOwnerCurrent, metadata: { threadName: resolveThreadBindingThreadName({ agentId: params.agentId, label }), agentId: params.agentId, label, boundBy: senderId || "unknown", introText: resolveThreadBindingIntroText({ agentId: params.agentId, label, idleTimeoutMs: resolveThreadBindingIdleTimeoutMsForChannel(lifecycleScope), maxAgeMs: resolveThreadBindingMaxAgeMsForChannel(lifecycleScope), sessionCwd: resolveAcpSessionCwd(params.sessionMeta), sessionDetails: resolveAcpThreadSessionDetailLines({ sessionKey: params.sessionKey, meta: params.sessionMeta }) }) } }; return { ok: true, bound: { binding: await bindingService.bind(bindingInput), 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 }); params.commandParams.command.assertOwnerCurrent?.(); 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 }, () => { params.commandParams.command.assertOwnerCurrent?.(); return { 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 acpCommandErrorReply(agentPolicyError, "ACP target agent is not allowed by policy.", "ACP_SESSION_INIT_FAILED"); 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 acpCommandErrorReply(error, "Could not resolve ACP session workspace.", "ACP_SESSION_INIT_FAILED"); } let initialized; try { initialized = await acpManager.initializeSession({ assertActive: params.command.assertOwnerCurrent, cfg: params.cfg, sessionKey, agentId: spawn.agentId, agent: spawn.agentId, mode: spawn.mode, cwd: runtimeCwd }); } catch (err) { return acpCommandErrorReply(err, "Could not initialize ACP session runtime.", "ACP_SESSION_INIT_FAILED"); } const { sessionEntry, closeRuntimeOnFailure, meta: initializedMeta } = initialized; const initializedBackend = initialized.handle.backend || initializedMeta.backend; const cleanupSpawn = () => cleanupFailedAcpSpawn({ cfg: params.cfg, sessionKey, agentId: spawn.agentId, sessionEntry, deleteTranscript: false, closeRuntimeOnFailure }); 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 cleanupSpawn(); return commandReply(`⚠️ ${result.error}`); } boundSession = result.bound; } try { await persistSpawnedSessionLabel({ commandParams: params, sessionKey, agentId: spawn.agentId, label: spawn.label }); } catch (err) { await cleanupSpawn(); 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 commandReply({ 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(" ")); } async function resolveAcpSessionForCommandOrStop(params) { const resolved = await params.acpManager.resolveSessionAsync({ cfg: params.cfg, sessionKey: params.sessionKey, agentId: params.agentId, assertCurrent: params.assertCurrent }); params.assertCurrent?.(); const error = resolveAcpSessionResolutionError(resolved); if (error) return acpCommandErrorReply(error, error.message, "ACP_SESSION_INIT_FAILED"); return null; } async function withResolvedAcpSessionTarget(params) { const acpManager = getAcpSessionManager(); const target = await resolveAcpTargetSessionKey({ commandParams: params.commandParams, token: params.token }); if (!target.ok) return commandReply(`⚠️ ${target.error}`); const guardFailure = await resolveAcpSessionForCommandOrStop({ acpManager, cfg: params.commandParams.cfg, ...target, assertCurrent: params.commandParams.command.assertOwnerCurrent }); params.commandParams.command.assertOwnerCurrent?.(); if (guardFailure) return guardFailure; return await params.run({ acpManager, ...target }); } async function handleAcpCancelAction(params, restTokens) { return await withResolvedAcpSessionTarget({ commandParams: params, token: normalizeOptionalString(restTokens.join(" ")), run: async ({ acpManager, sessionKey, agentId }) => await withAcpCommandErrorBoundary({ run: async () => { await acpManager.cancelSession({ assertActive: params.command.assertOwnerCurrent, cfg: params.cfg, sessionKey, agentId, reason: "manual-cancel" }); return commandReply(`✅ Cancel requested for ACP session ${sessionKey}.`); }, fallbackMessage: "ACP cancel failed before completion." }) }); } async function handleAcpSteerAction(params, restTokens) { const dispatchPolicyError = resolveAcpDispatchPolicyError(params.cfg); if (dispatchPolicyError) return acpCommandErrorReply(dispatchPolicyError, dispatchPolicyError.message, "ACP_DISPATCH_DISABLED"); const parsed = parseSteerInput(restTokens); if (!parsed.ok) return commandReply(`⚠️ ${parsed.error}`); return await withResolvedAcpSessionTarget({ commandParams: params, token: parsed.value.sessionToken, run: async ({ sessionKey, agentId }) => withAcpCommandErrorBoundary({ run: async () => { const requestId = `${resolveCommandRequestId(params)}:steer`; const steeringManager = getAcpSessionManager(); let output = ""; const admittedRunContext = await prepareChannelRunAdmission({ assertSourceCurrent: params.command.assertOwnerCurrent, cfg: params.cfg, runId: requestId, agentId, ingressKind: "acp", boundary: "acp.command.steer", evidence: readChannelContextAdmissionEvidence(params.rootCtx ?? params.ctx), gatewayLocalUserIngress: getGatewayLocalUserIngress(params.rootCtx ?? params.ctx) }).admit("acp"); try { await steeringManager.runTurn({ admittedRunContext, cfg: params.cfg, sessionKey, agentId, provenance: "agent", text: parsed.value.instruction, mode: "steer", requestId, onEvent: (event) => { if (event.type !== "text_delta" || event.stream && event.stream !== "output") return; if (event.text) { output += event.text; if (output.length > 800) output = `${truncateUtf16Safe(output, 800)}…`; } } }); } finally { closeAdmittedRunDelegatedAuthority(admittedRunContext); } const steerOutput = output.trim(); return commandReply(`✅ ACP steer sent to ${sessionKey}.${steerOutput ? `\n${steerOutput}` : ""}`); }, fallbackMessage: "ACP steer failed before completion." }) }); } async function handleAcpCloseAction(params, restTokens) { return await withResolvedAcpSessionTarget({ commandParams: params, token: normalizeOptionalString(restTokens.join(" ")), run: async ({ acpManager, sessionKey, agentId }) => { let runtimeNotice; try { const closed = await acpManager.closeSession({ assertActive: params.command.assertOwnerCurrent, cfg: params.cfg, sessionKey, agentId, reason: "manual-close", allowBackendUnavailable: true, clearMeta: true }); runtimeNotice = closed.runtimeNotice ? ` (${closed.runtimeNotice})` : ""; } catch (error) { return acpCommandErrorReply(error, "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 };