File size: 4,300 Bytes
5c2a829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Private ACPX runtime backend implementation.
// The public ACP runtime and shipped compatibility facade project this narrow owner surface.

import { hasExplicitCommandContextText } from "../auto-reply/reply/context-text.js";
import {
  finalizeInboundContextForSdk,
  isFinalizedInboundContext,
} from "../auto-reply/reply/inbound-context.js";
import type {
  PluginHookReplyDispatchContext,
  PluginHookReplyDispatchEvent,
  PluginHookReplyDispatchResult,
} from "../plugins/types.js";
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";

export { AcpRuntimeError, isAcpRuntimeError } from "../acp/runtime/errors.js";
export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js";
export {
  getAcpRuntimeBackend,
  registerAcpRuntimeBackend,
  requireAcpRuntimeBackend,
  unregisterAcpRuntimeBackend,
} from "../acp/runtime/registry.js";
export type {
  AcpRuntime,
  AcpRuntimeCapabilities,
  AcpRuntimeConfigOptionResult,
  AcpRuntimeDoctorReport,
  AcpRuntimeEnsureInput,
  AcpRuntimeEvent,
  AcpRuntimeHandle,
  AcpRuntimeStatus,
  AcpRuntimeTurn,
  AcpRuntimeTurnAttachment,
  AcpRuntimeTurnInput,
  AcpRuntimeTurnResult,
  AcpRuntimeTurnResultError,
  AcpSessionUpdateTag,
} from "@openclaw/acp-core/runtime/types";

// ACP dispatch pulls in session/media/manager code; keep it lazy so
// startup-loaded plugin surfaces stay light and concurrent hooks share one load.
const loadDispatchAcpRuntime = createLazyRuntimeModule(
  () => import("../auto-reply/reply/dispatch-acp.runtime.js"),
);

/**
 * Dispatch a plugin reply hook through ACP when the event targets an ACP-bound session.
 * Returns a handled result only when ACP consumes the reply; otherwise callers continue normal delivery.
 */
export async function tryDispatchAcpReplyHook(
  event: PluginHookReplyDispatchEvent,
  ctx: PluginHookReplyDispatchContext,
): Promise<PluginHookReplyDispatchResult | void> {
  const finalizedCtx = isFinalizedInboundContext(event.ctx)
    ? event.ctx
    : finalizeInboundContextForSdk(event.ctx);
  // Under sendPolicy: "deny", ACP-bound sessions still need their turns to flow
  // through acpManager.runTurn so session state, tool calls, and memory stay
  // consistent. Delivery suppression is handled by the ACP delivery path.
  if (
    event.sendPolicy === "deny" &&
    !event.suppressUserDelivery &&
    !hasExplicitCommandContextText(finalizedCtx) &&
    !event.isTailDispatch
  ) {
    return;
  }
  const runtime = await loadDispatchAcpRuntime();
  const bypassForCommand = await runtime.shouldBypassAcpDispatchForCommand(finalizedCtx, ctx.cfg);

  if (
    event.sendPolicy === "deny" &&
    !event.suppressUserDelivery &&
    !bypassForCommand &&
    !event.isTailDispatch
  ) {
    return;
  }

  const result = await runtime.tryDispatchAcpReply({
    ctx: finalizedCtx,
    cfg: ctx.cfg,
    dispatcher: ctx.dispatcher,
    runId: event.runId,
    sessionKey: event.sessionKey,
    toolsAllow: event.toolsAllow,
    images: event.images,
    abortSignal: ctx.abortSignal,
    inboundAudio: event.inboundAudio,
    sessionTtsAuto: event.sessionTtsAuto,
    ttsChannel: event.ttsChannel,
    suppressUserDelivery: event.suppressUserDelivery,
    suppressReplyLifecycle: event.suppressReplyLifecycle === true || event.sendPolicy === "deny",
    sourceReplyDeliveryMode: event.sourceReplyDeliveryMode,
    shouldRouteToOriginating: event.shouldRouteToOriginating,
    originatingChannel: event.originatingChannel,
    originatingTo: event.originatingTo,
    originatingAccountId: event.originatingAccountId,
    originatingThreadId: event.originatingThreadId,
    originatingChatType: event.originatingChatType,
    shouldSendToolSummaries: event.shouldSendToolSummaries,
    shouldSendToolSummariesNow: () => event.shouldSendToolSummaries,
    shouldSendFullToolDetails: event.shouldSendFullToolDetails,
    bypassForCommand,
    onReplyStart: ctx.onReplyStart,
    onAgentRunStart: ctx.onAgentRunStart,
    userTurnTranscriptRecorder: ctx.userTurnTranscriptRecorder,
    prepareAssistantTranscriptMessage: ctx.prepareAssistantTranscriptMessage,
    recordProcessed: ctx.recordProcessed,
    markIdle: ctx.markIdle,
  });

  if (!result) {
    return;
  }

  return {
    handled: true,
    queuedFinal: result.queuedFinal,
    counts: result.counts,
  };
}