refactor(hooks): centralize bundled subagent hook wiring

This commit is contained in:
Vincent Koc
2026-04-22 11:36:07 -07:00
parent d30f252c1b
commit bbcd185215
8 changed files with 97 additions and 91 deletions

View File

@@ -1,13 +1,5 @@
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
type DiscordSubagentHooksModule = typeof import("./subagent-hooks-api.js");
let discordSubagentHooksPromise: Promise<DiscordSubagentHooksModule> | null = null;
function loadDiscordSubagentHooksModule() {
discordSubagentHooksPromise ??= import("./subagent-hooks-api.js");
return discordSubagentHooksPromise;
}
import { registerDiscordSubagentHooks } from "./subagent-hooks-api.js";
export default defineBundledChannelEntry({
id: "discord",
@@ -27,17 +19,6 @@ export default defineBundledChannelEntry({
exportName: "inspectDiscordReadOnlyAccount",
},
registerFull(api) {
api.on("subagent_spawning", async (event) => {
const { handleDiscordSubagentSpawning } = await loadDiscordSubagentHooksModule();
return await handleDiscordSubagentSpawning(api, event);
});
api.on("subagent_ended", async (event) => {
const { handleDiscordSubagentEnded } = await loadDiscordSubagentHooksModule();
handleDiscordSubagentEnded(event);
});
api.on("subagent_delivery_target", async (event) => {
const { handleDiscordSubagentDeliveryTarget } = await loadDiscordSubagentHooksModule();
return handleDiscordSubagentDeliveryTarget(event);
});
registerDiscordSubagentHooks(api);
},
});

View File

@@ -38,7 +38,7 @@ const hookMocks = vi.hoisted(() => ({
unbindThreadBindingsBySessionKey: vi.fn(() => []),
}));
let registerDiscordSubagentHooks: typeof import("./subagent-hooks.js").registerDiscordSubagentHooks;
let registerDiscordSubagentHooks: typeof import("../subagent-hooks-api.js").registerDiscordSubagentHooks;
vi.mock("./accounts.js", () => ({
resolveDiscordAccount: hookMocks.resolveDiscordAccount,
@@ -66,7 +66,7 @@ function registerHandlersForTest(
});
}
function resolveSubagentDeliveryTargetForTest(requesterOrigin: {
async function resolveSubagentDeliveryTargetForTest(requesterOrigin: {
channel: string;
accountId: string;
to: string;
@@ -74,7 +74,7 @@ function resolveSubagentDeliveryTargetForTest(requesterOrigin: {
}) {
const handlers = registerHandlersForTest();
const handler = getRequiredHookHandler(handlers, "subagent_delivery_target");
return handler(
return await handler(
{
childSessionKey: "agent:main:subagent:child",
requesterSessionKey: "agent:main:main",
@@ -167,7 +167,7 @@ async function expectSubagentSpawningError(params?: {
describe("discord subagent hook handlers", () => {
beforeAll(async () => {
({ registerDiscordSubagentHooks } = await import("./subagent-hooks.js"));
({ registerDiscordSubagentHooks } = await import("../subagent-hooks-api.js"));
});
beforeEach(() => {
@@ -303,11 +303,11 @@ describe("discord subagent hook handlers", () => {
expect(errorText).toMatch(/unable to create or bind/i);
});
it("unbinds thread routing on subagent_ended", () => {
it("unbinds thread routing on subagent_ended", async () => {
const handlers = registerHandlersForTest();
const handler = getRequiredHookHandler(handlers, "subagent_ended");
handler(
await handler(
{
targetSessionKey: "agent:main:subagent:child",
targetKind: "subagent",
@@ -328,11 +328,11 @@ describe("discord subagent hook handlers", () => {
});
});
it("resolves delivery target from matching bound thread", () => {
it("resolves delivery target from matching bound thread", async () => {
hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
{ accountId: "work", threadId: "777" },
]);
const result = resolveSubagentDeliveryTargetForTest({
const result = await resolveSubagentDeliveryTargetForTest({
channel: "discord",
accountId: "work",
to: "channel:123",
@@ -354,12 +354,12 @@ describe("discord subagent hook handlers", () => {
});
});
it("keeps original routing when delivery target is ambiguous", () => {
it("keeps original routing when delivery target is ambiguous", async () => {
hookMocks.listThreadBindingsBySessionKey.mockReturnValueOnce([
{ accountId: "work", threadId: "777" },
{ accountId: "work", threadId: "888" },
]);
const result = resolveSubagentDeliveryTargetForTest({
const result = await resolveSubagentDeliveryTargetForTest({
channel: "discord",
accountId: "work",
to: "channel:123",

View File

@@ -212,9 +212,3 @@ export function handleDiscordSubagentDeliveryTarget(
},
};
}
export function registerDiscordSubagentHooks(api: OpenClawPluginApi) {
api.on("subagent_spawning", (event) => handleDiscordSubagentSpawning(api, event));
api.on("subagent_ended", (event) => handleDiscordSubagentEnded(event));
api.on("subagent_delivery_target", (event) => handleDiscordSubagentDeliveryTarget(event));
}

View File

@@ -1,5 +1,31 @@
// Subagent hooks live behind a dedicated barrel so the bundled entry can lazy
// load only the handlers it needs.
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contract";
type DiscordSubagentHooksModule = typeof import("./src/subagent-hooks.js");
let discordSubagentHooksPromise: Promise<DiscordSubagentHooksModule> | null = null;
function loadDiscordSubagentHooksModule() {
discordSubagentHooksPromise ??= import("./src/subagent-hooks.js");
return discordSubagentHooksPromise;
}
// Subagent hooks live behind a dedicated barrel so the bundled entry can
// register one stable hook wiring path while keeping the handler module lazy.
export function registerDiscordSubagentHooks(api: OpenClawPluginApi): void {
api.on("subagent_spawning", async (event) => {
const { handleDiscordSubagentSpawning } = await loadDiscordSubagentHooksModule();
return await handleDiscordSubagentSpawning(api, event);
});
api.on("subagent_ended", async (event) => {
const { handleDiscordSubagentEnded } = await loadDiscordSubagentHooksModule();
handleDiscordSubagentEnded(event);
});
api.on("subagent_delivery_target", async (event) => {
const { handleDiscordSubagentDeliveryTarget } = await loadDiscordSubagentHooksModule();
return handleDiscordSubagentDeliveryTarget(event);
});
}
export {
handleDiscordSubagentDeliveryTarget,
handleDiscordSubagentEnded,

View File

@@ -3,15 +3,7 @@ import {
loadBundledEntryExportSync,
} from "openclaw/plugin-sdk/channel-entry-contract";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contract";
type FeishuSubagentHooksModule = typeof import("./api.js");
let feishuSubagentHooksPromise: Promise<FeishuSubagentHooksModule> | null = null;
function loadFeishuSubagentHooksModule() {
feishuSubagentHooksPromise ??= import("./api.js");
return feishuSubagentHooksPromise;
}
import { registerFeishuSubagentHooks } from "./subagent-hooks-api.js";
function registerFeishuDocTools(api: OpenClawPluginApi) {
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
@@ -79,18 +71,7 @@ export default defineBundledChannelEntry({
exportName: "setFeishuRuntime",
},
registerFull(api) {
api.on("subagent_spawning", async (event, ctx) => {
const { handleFeishuSubagentSpawning } = await loadFeishuSubagentHooksModule();
return await handleFeishuSubagentSpawning(event, ctx);
});
api.on("subagent_delivery_target", async (event) => {
const { handleFeishuSubagentDeliveryTarget } = await loadFeishuSubagentHooksModule();
return handleFeishuSubagentDeliveryTarget(event);
});
api.on("subagent_ended", async (event) => {
const { handleFeishuSubagentEnded } = await loadFeishuSubagentHooksModule();
handleFeishuSubagentEnded(event);
});
registerFeishuSubagentHooks(api);
registerFeishuDocTools(api);
registerFeishuChatTools(api);
registerFeishuWikiTools(api);

View File

@@ -4,7 +4,7 @@ import {
registerHookHandlersForTest,
} from "../../../test/helpers/plugins/subagent-hooks.js";
import type { ClawdbotConfig, OpenClawPluginApi } from "../runtime-api.js";
import { registerFeishuSubagentHooks } from "./subagent-hooks.js";
import { registerFeishuSubagentHooks } from "../subagent-hooks-api.js";
import {
createFeishuThreadBindingManager,
__testing as threadBindingTesting,
@@ -51,7 +51,7 @@ describe("feishu subagent hook handlers", () => {
expect(result).toEqual({ status: "ok", threadBindingReady: true });
const deliveryTargetHandler = getRequiredHookHandler(handlers, "subagent_delivery_target");
expect(
await expect(
deliveryTargetHandler(
{
childSessionKey: "agent:main:subagent:child",
@@ -65,7 +65,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toEqual({
).resolves.toEqual({
origin: {
channel: "feishu",
accountId: "work",
@@ -89,7 +89,7 @@ describe("feishu subagent hook handlers", () => {
},
});
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:chat-dm-child",
@@ -103,7 +103,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toEqual({
).resolves.toEqual({
origin: {
channel: "feishu",
accountId: "work",
@@ -136,7 +136,7 @@ describe("feishu subagent hook handlers", () => {
);
expect(result).toEqual({ status: "ok", threadBindingReady: true });
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:topic-child",
@@ -151,7 +151,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toEqual({
).resolves.toEqual({
origin: {
channel: "feishu",
accountId: "work",
@@ -205,7 +205,7 @@ describe("feishu subagent hook handlers", () => {
parentConversationId: "oc_group_chat",
},
]);
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:sender-child",
@@ -220,7 +220,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toEqual({
).resolves.toEqual({
origin: {
channel: "feishu",
accountId: "work",
@@ -267,7 +267,7 @@ describe("feishu subagent hook handlers", () => {
{},
);
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:shared",
@@ -281,7 +281,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toEqual({
).resolves.toEqual({
origin: {
channel: "feishu",
accountId: "work",
@@ -335,7 +335,7 @@ describe("feishu subagent hook handlers", () => {
error: expect.stringContaining("direct messages or topic conversations"),
});
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:ambiguous-child",
@@ -350,7 +350,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
});
it("fails closed when both topic-level and sender-scoped requester bindings exist", async () => {
@@ -398,7 +398,7 @@ describe("feishu subagent hook handlers", () => {
error: expect.stringContaining("direct messages or topic conversations"),
});
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:mixed-topic-child",
@@ -413,7 +413,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
});
it("no-ops for non-Feishu channels and non-threaded spawns", async () => {
@@ -456,7 +456,7 @@ describe("feishu subagent hook handlers", () => {
),
).resolves.toBeUndefined();
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:child",
@@ -470,9 +470,9 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
expect(
await expect(
endedHandler(
{
targetSessionKey: "agent:main:subagent:child",
@@ -482,7 +482,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
});
it("returns an error for unsupported non-topic Feishu group conversations", async () => {
@@ -532,7 +532,7 @@ describe("feishu subagent hook handlers", () => {
{},
);
endedHandler(
await endedHandler(
{
targetSessionKey: "agent:main:subagent:child",
targetKind: "subagent",
@@ -542,7 +542,7 @@ describe("feishu subagent hook handlers", () => {
{},
);
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:child",
@@ -556,7 +556,7 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
});
it("fails closed when the Feishu monitor-owned binding manager is unavailable", async () => {
@@ -584,7 +584,7 @@ describe("feishu subagent hook handlers", () => {
error: expect.stringContaining("monitor is not active"),
});
expect(
await expect(
deliveryHandler(
{
childSessionKey: "agent:main:subagent:no-manager",
@@ -598,6 +598,6 @@ describe("feishu subagent hook handlers", () => {
},
{},
),
).toBeUndefined();
).resolves.toBeUndefined();
});
});

View File

@@ -2,7 +2,6 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import type { OpenClawPluginApi } from "../runtime-api.js";
import { buildFeishuConversationId, parseFeishuConversationId } from "./conversation-id.js";
import { normalizeFeishuTarget } from "./targets.js";
import { getFeishuThreadBindingManager } from "./thread-bindings.js";
@@ -396,9 +395,3 @@ export function handleFeishuSubagentEnded(event: FeishuSubagentEndedEvent) {
const manager = getFeishuThreadBindingManager(event.accountId);
manager?.unbindBySessionKey(event.targetSessionKey);
}
export function registerFeishuSubagentHooks(api: OpenClawPluginApi) {
api.on("subagent_spawning", (event, ctx) => handleFeishuSubagentSpawning(event, ctx));
api.on("subagent_delivery_target", (event) => handleFeishuSubagentDeliveryTarget(event));
api.on("subagent_ended", (event) => handleFeishuSubagentEnded(event));
}

View File

@@ -0,0 +1,31 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contract";
type FeishuSubagentHooksModule = typeof import("./src/subagent-hooks.js");
let feishuSubagentHooksPromise: Promise<FeishuSubagentHooksModule> | null = null;
function loadFeishuSubagentHooksModule() {
feishuSubagentHooksPromise ??= import("./src/subagent-hooks.js");
return feishuSubagentHooksPromise;
}
export function registerFeishuSubagentHooks(api: OpenClawPluginApi): void {
api.on("subagent_spawning", async (event, ctx) => {
const { handleFeishuSubagentSpawning } = await loadFeishuSubagentHooksModule();
return await handleFeishuSubagentSpawning(event, ctx);
});
api.on("subagent_delivery_target", async (event) => {
const { handleFeishuSubagentDeliveryTarget } = await loadFeishuSubagentHooksModule();
return handleFeishuSubagentDeliveryTarget(event);
});
api.on("subagent_ended", async (event) => {
const { handleFeishuSubagentEnded } = await loadFeishuSubagentHooksModule();
handleFeishuSubagentEnded(event);
});
}
export {
handleFeishuSubagentDeliveryTarget,
handleFeishuSubagentEnded,
handleFeishuSubagentSpawning,
} from "./src/subagent-hooks.js";