Files
openclaw/src/plugin-sdk/acp-runtime.ts
2026-04-06 02:43:14 +01:00

137 lines
4.2 KiB
TypeScript

// Public ACP runtime helpers for plugins that integrate with ACP control/session state.
import { __testing as managerTesting, getAcpSessionManager } from "../acp/control-plane/manager.js";
import { __testing as registryTesting } from "../acp/runtime/registry.js";
import type {
PluginHookReplyDispatchContext,
PluginHookReplyDispatchEvent,
PluginHookReplyDispatchResult,
} from "../plugins/types.js";
export { getAcpSessionManager };
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,
AcpRuntimeDoctorReport,
AcpRuntimeEnsureInput,
AcpRuntimeEvent,
AcpRuntimeHandle,
AcpRuntimeStatus,
AcpRuntimeTurnAttachment,
AcpRuntimeTurnInput,
AcpSessionUpdateTag,
} from "../acp/runtime/types.js";
export { readAcpSessionEntry } from "../acp/runtime/session-meta.js";
export type { AcpSessionStoreEntry } from "../acp/runtime/session-meta.js";
let dispatchAcpRuntimePromise: Promise<
typeof import("../auto-reply/reply/dispatch-acp.runtime.js")
> | null = null;
function loadDispatchAcpRuntime() {
dispatchAcpRuntimePromise ??= import("../auto-reply/reply/dispatch-acp.runtime.js");
return dispatchAcpRuntimePromise;
}
function hasExplicitCommandCandidate(ctx: PluginHookReplyDispatchEvent["ctx"]): boolean {
const commandBody = ctx.CommandBody;
if (typeof commandBody === "string" && commandBody.trim().length > 0) {
return true;
}
const bodyForCommands = ctx.BodyForCommands;
if (typeof bodyForCommands !== "string") {
return false;
}
const normalized = bodyForCommands.trim();
if (!normalized) {
return false;
}
return normalized.startsWith("!") || normalized.startsWith("/");
}
export async function tryDispatchAcpReplyHook(
event: PluginHookReplyDispatchEvent,
ctx: PluginHookReplyDispatchContext,
): Promise<PluginHookReplyDispatchResult | void> {
if (event.sendPolicy === "deny" && !hasExplicitCommandCandidate(event.ctx)) {
return;
}
const runtime = await loadDispatchAcpRuntime();
const bypassForCommand = await runtime.shouldBypassAcpDispatchForCommand(event.ctx, ctx.cfg);
if (event.sendPolicy === "deny" && !bypassForCommand) {
return;
}
const result = await runtime.tryDispatchAcpReply({
ctx: event.ctx,
cfg: ctx.cfg,
dispatcher: ctx.dispatcher,
runId: event.runId,
sessionKey: event.sessionKey,
abortSignal: ctx.abortSignal,
inboundAudio: event.inboundAudio,
sessionTtsAuto: event.sessionTtsAuto,
ttsChannel: event.ttsChannel,
suppressUserDelivery: event.suppressUserDelivery,
shouldRouteToOriginating: event.shouldRouteToOriginating,
originatingChannel: event.originatingChannel,
originatingTo: event.originatingTo,
shouldSendToolSummaries: event.shouldSendToolSummaries,
bypassForCommand,
onReplyStart: ctx.onReplyStart,
recordProcessed: ctx.recordProcessed,
markIdle: ctx.markIdle,
});
if (!result) {
return;
}
return {
handled: true,
queuedFinal: result.queuedFinal,
counts: result.counts,
};
}
// Keep test helpers off the hot init path. Eagerly merging them here can
// create a back-edge through the bundled ACP runtime chunk before the imported
// testing bindings finish initialization.
export const __testing = new Proxy({} as typeof managerTesting & typeof registryTesting, {
get(_target, prop, receiver) {
if (Reflect.has(managerTesting, prop)) {
return Reflect.get(managerTesting, prop, receiver);
}
return Reflect.get(registryTesting, prop, receiver);
},
has(_target, prop) {
return Reflect.has(managerTesting, prop) || Reflect.has(registryTesting, prop);
},
ownKeys() {
return Array.from(
new Set([...Reflect.ownKeys(managerTesting), ...Reflect.ownKeys(registryTesting)]),
);
},
getOwnPropertyDescriptor(_target, prop) {
if (Reflect.has(managerTesting, prop) || Reflect.has(registryTesting, prop)) {
return {
configurable: true,
enumerable: true,
};
}
return undefined;
},
});