mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
feat(plugins): expose hook correlation fields
Expose first-class hook correlation fields for plugin message and run lifecycle hooks, including frozen diagnostic trace copies for plugin-facing events.
This commit is contained in:
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gradium: add a bundled text-to-speech provider with voice-note and telephony output support. (#64958) Thanks @LaurentMazare.
|
||||
- Plugins/setup: honor explicit `setup.requiresRuntime: false` as a descriptor-only setup contract while keeping omitted values on the legacy setup-api fallback path. Thanks @vincentkoc.
|
||||
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
|
||||
- Plugin hooks: expose first-class run, message, sender, session, and trace correlation fields on message hook contexts and run lifecycle events. Thanks @vincentkoc.
|
||||
- TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc.
|
||||
- Plugins/activation: expose activation plan reasons and a richer plan API so callers can inspect why a plugin was selected while preserving existing id-list activation behavior. (#70943) Thanks @vincentkoc.
|
||||
|
||||
@@ -159,6 +159,9 @@ Use the phase-specific hooks for new plugins:
|
||||
`before_agent_start` remains for compatibility. Prefer the explicit hooks above
|
||||
so your plugin does not depend on a legacy combined phase.
|
||||
|
||||
`before_agent_start` and `agent_end` include `event.runId` when OpenClaw can
|
||||
identify the active run. The same value is also available on `ctx.runId`.
|
||||
|
||||
Non-bundled plugins that need `llm_input`, `llm_output`, or `agent_end` must set:
|
||||
|
||||
```json
|
||||
@@ -182,10 +185,16 @@ Prompt-mutating hooks can be disabled per plugin with
|
||||
|
||||
Use message hooks for channel-level routing and delivery policy:
|
||||
|
||||
- `message_received`: observe inbound content, sender, `threadId`, and metadata.
|
||||
- `message_received`: observe inbound content, sender, `threadId`, `messageId`,
|
||||
`senderId`, optional run/session correlation, and metadata.
|
||||
- `message_sending`: rewrite `content` or return `{ cancel: true }`.
|
||||
- `message_sent`: observe final success or failure.
|
||||
|
||||
Message hook contexts expose stable correlation fields when available:
|
||||
`ctx.sessionKey`, `ctx.runId`, `ctx.messageId`, `ctx.senderId`, `ctx.trace`,
|
||||
`ctx.traceId`, `ctx.spanId`, `ctx.parentSpanId`, and `ctx.callDepth`. Prefer
|
||||
these first-class fields before reading legacy metadata.
|
||||
|
||||
Prefer typed `threadId` and `replyToId` fields before using channel-specific
|
||||
metadata.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ function makeInboundCtx(overrides: Partial<FinalizedMsgContext> = {}): Finalized
|
||||
Surface: "demo-chat",
|
||||
OriginatingChannel: "demo-chat",
|
||||
OriginatingTo: "demo-chat:chat:456",
|
||||
SessionKey: "session-1",
|
||||
AccountId: "acc-1",
|
||||
MessageSid: "msg-1",
|
||||
SenderId: "sender-1",
|
||||
@@ -141,18 +143,50 @@ describe("message hook mappers", () => {
|
||||
});
|
||||
|
||||
it("maps canonical inbound context to plugin/internal received payloads", () => {
|
||||
const canonical = deriveInboundMessageHookContext(makeInboundCtx({ TopicName: "Deployments" }));
|
||||
const trace: DiagnosticTraceContext = {
|
||||
traceId: "11111111111111111111111111111111",
|
||||
spanId: "2222222222222222",
|
||||
parentSpanId: "3333333333333333",
|
||||
};
|
||||
const canonical = {
|
||||
...deriveInboundMessageHookContext(makeInboundCtx({ TopicName: "Deployments" })),
|
||||
runId: "run-1",
|
||||
trace,
|
||||
callDepth: 2,
|
||||
};
|
||||
|
||||
expect(toPluginMessageContext(canonical)).toEqual({
|
||||
const pluginContext = toPluginMessageContext(canonical);
|
||||
const receivedEvent = toPluginMessageReceivedEvent(canonical);
|
||||
expect(pluginContext).toEqual({
|
||||
channelId: "demo-chat",
|
||||
accountId: "acc-1",
|
||||
conversationId: "demo-chat:chat:456",
|
||||
sessionKey: "session-1",
|
||||
runId: "run-1",
|
||||
messageId: "msg-1",
|
||||
senderId: "sender-1",
|
||||
trace,
|
||||
traceId: "11111111111111111111111111111111",
|
||||
spanId: "2222222222222222",
|
||||
parentSpanId: "3333333333333333",
|
||||
callDepth: 2,
|
||||
});
|
||||
expect(toPluginMessageReceivedEvent(canonical)).toEqual({
|
||||
expect(pluginContext.trace).not.toBe(trace);
|
||||
expect(pluginContext.trace).toEqual(trace);
|
||||
expect(Object.isFrozen(pluginContext.trace)).toBe(true);
|
||||
expect(receivedEvent).toEqual({
|
||||
from: "demo-chat:user:123",
|
||||
content: "commands-body",
|
||||
timestamp: 1710000000,
|
||||
threadId: 42,
|
||||
messageId: "msg-1",
|
||||
senderId: "sender-1",
|
||||
sessionKey: "session-1",
|
||||
runId: "run-1",
|
||||
trace,
|
||||
traceId: "11111111111111111111111111111111",
|
||||
spanId: "2222222222222222",
|
||||
parentSpanId: "3333333333333333",
|
||||
metadata: expect.objectContaining({
|
||||
messageId: "msg-1",
|
||||
senderName: "User One",
|
||||
@@ -160,6 +194,9 @@ describe("message hook mappers", () => {
|
||||
topicName: "Deployments",
|
||||
}),
|
||||
});
|
||||
expect(receivedEvent.trace).not.toBe(trace);
|
||||
expect(receivedEvent.trace).toEqual(trace);
|
||||
expect(Object.isFrozen(receivedEvent.trace)).toBe(true);
|
||||
expect(toInternalMessageReceivedContext(canonical)).toEqual({
|
||||
from: "demo-chat:user:123",
|
||||
content: "commands-body",
|
||||
@@ -176,6 +213,39 @@ describe("message hook mappers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes frozen trace copies to inbound claim and sent plugin hooks", () => {
|
||||
const trace: DiagnosticTraceContext = {
|
||||
traceId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
spanId: "bbbbbbbbbbbbbbbb",
|
||||
parentSpanId: "cccccccccccccccc",
|
||||
traceFlags: "01",
|
||||
};
|
||||
const inbound = {
|
||||
...deriveInboundMessageHookContext(makeInboundCtx()),
|
||||
trace,
|
||||
};
|
||||
const inboundContext = toPluginInboundClaimContext(inbound);
|
||||
const inboundEvent = toPluginInboundClaimEvent(inbound);
|
||||
expect(inboundContext.trace).not.toBe(trace);
|
||||
expect(inboundContext.trace).toEqual(trace);
|
||||
expect(Object.isFrozen(inboundContext.trace)).toBe(true);
|
||||
expect(inboundEvent.trace).not.toBe(trace);
|
||||
expect(inboundEvent.trace).toEqual(trace);
|
||||
expect(Object.isFrozen(inboundEvent.trace)).toBe(true);
|
||||
|
||||
const sent = buildCanonicalSentMessageHookContext({
|
||||
to: "demo-chat:chat:456",
|
||||
content: "reply",
|
||||
success: true,
|
||||
channelId: "demo-chat",
|
||||
trace,
|
||||
});
|
||||
const sentEvent = toPluginMessageSentEvent(sent);
|
||||
expect(sentEvent.trace).not.toBe(trace);
|
||||
expect(sentEvent.trace).toEqual(trace);
|
||||
expect(Object.isFrozen(sentEvent.trace)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses channel plugin claim resolvers for grouped conversations", () => {
|
||||
const canonical = deriveInboundMessageHookContext(
|
||||
makeInboundCtx({
|
||||
@@ -193,9 +263,16 @@ describe("message hook mappers", () => {
|
||||
channelId: "claim-chat",
|
||||
accountId: "acc-1",
|
||||
conversationId: "channel:123456789012345678",
|
||||
sessionKey: "session-1",
|
||||
parentConversationId: undefined,
|
||||
senderId: "sender-1",
|
||||
messageId: "msg-1",
|
||||
runId: undefined,
|
||||
trace: undefined,
|
||||
traceId: undefined,
|
||||
spanId: undefined,
|
||||
parentSpanId: undefined,
|
||||
callDepth: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,9 +294,16 @@ describe("message hook mappers", () => {
|
||||
channelId: "claim-chat",
|
||||
accountId: "acc-1",
|
||||
conversationId: "user:1177378744822943744",
|
||||
sessionKey: "session-1",
|
||||
parentConversationId: undefined,
|
||||
senderId: "sender-1",
|
||||
messageId: "msg-1",
|
||||
runId: undefined,
|
||||
trace: undefined,
|
||||
traceId: undefined,
|
||||
spanId: undefined,
|
||||
parentSpanId: undefined,
|
||||
callDepth: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,7 +330,9 @@ describe("message hook mappers", () => {
|
||||
error: "network error",
|
||||
channelId: "demo-chat",
|
||||
accountId: "acc-1",
|
||||
sessionKey: "session-1",
|
||||
messageId: "out-1",
|
||||
runId: "run-out-1",
|
||||
isGroup: true,
|
||||
groupId: "demo-chat:chat:456",
|
||||
});
|
||||
@@ -255,11 +341,17 @@ describe("message hook mappers", () => {
|
||||
channelId: "demo-chat",
|
||||
accountId: "acc-1",
|
||||
conversationId: "demo-chat:chat:456",
|
||||
sessionKey: "session-1",
|
||||
runId: "run-out-1",
|
||||
messageId: "out-1",
|
||||
});
|
||||
expect(toPluginMessageSentEvent(canonical)).toEqual({
|
||||
to: "demo-chat:chat:456",
|
||||
content: "reply",
|
||||
success: false,
|
||||
messageId: "out-1",
|
||||
sessionKey: "session-1",
|
||||
runId: "run-out-1",
|
||||
error: "network error",
|
||||
});
|
||||
expect(toInternalMessageSentContext(canonical)).toEqual({
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
freezeDiagnosticTraceContext,
|
||||
type DiagnosticTraceContext,
|
||||
} from "../infra/diagnostic-trace-context.js";
|
||||
import type {
|
||||
PluginHookInboundClaimContext,
|
||||
PluginHookInboundClaimEvent,
|
||||
@@ -30,6 +34,8 @@ export type CanonicalInboundMessageHookContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
messageId?: string;
|
||||
senderId?: string;
|
||||
senderName?: string;
|
||||
@@ -53,6 +59,8 @@ export type CanonicalInboundMessageHookContext = {
|
||||
isGroup: boolean;
|
||||
groupId?: string;
|
||||
topicName?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
callDepth?: number;
|
||||
};
|
||||
|
||||
export type CanonicalSentMessageHookContext = {
|
||||
@@ -63,7 +71,11 @@ export type CanonicalSentMessageHookContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
messageId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
callDepth?: number;
|
||||
isGroup?: boolean;
|
||||
groupId?: string;
|
||||
};
|
||||
@@ -118,6 +130,7 @@ export function deriveInboundMessageHookContext(
|
||||
channelId,
|
||||
accountId: ctx.AccountId,
|
||||
conversationId,
|
||||
sessionKey: ctx.SessionKey,
|
||||
messageId:
|
||||
overrides?.messageId ??
|
||||
ctx.MessageSidFull ??
|
||||
@@ -155,7 +168,11 @@ export function buildCanonicalSentMessageHookContext(params: {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
messageId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
callDepth?: number;
|
||||
isGroup?: boolean;
|
||||
groupId?: string;
|
||||
}): CanonicalSentMessageHookContext {
|
||||
@@ -167,20 +184,64 @@ export function buildCanonicalSentMessageHookContext(params: {
|
||||
channelId: params.channelId,
|
||||
accountId: params.accountId,
|
||||
conversationId: params.conversationId ?? params.to,
|
||||
sessionKey: params.sessionKey,
|
||||
runId: params.runId,
|
||||
messageId: params.messageId,
|
||||
trace: params.trace,
|
||||
callDepth: params.callDepth,
|
||||
isGroup: params.isGroup,
|
||||
groupId: params.groupId,
|
||||
};
|
||||
}
|
||||
|
||||
type DiagnosticTraceHookFields = Pick<
|
||||
PluginHookMessageContext,
|
||||
"trace" | "traceId" | "spanId" | "parentSpanId"
|
||||
>;
|
||||
|
||||
function assignTraceFields(
|
||||
target: DiagnosticTraceHookFields,
|
||||
trace?: DiagnosticTraceContext,
|
||||
): void {
|
||||
if (!trace) {
|
||||
return;
|
||||
}
|
||||
const safeTrace = freezeDiagnosticTraceContext(trace);
|
||||
target.trace = safeTrace;
|
||||
target.traceId = safeTrace.traceId;
|
||||
if (safeTrace.spanId) {
|
||||
target.spanId = safeTrace.spanId;
|
||||
}
|
||||
if (safeTrace.parentSpanId) {
|
||||
target.parentSpanId = safeTrace.parentSpanId;
|
||||
}
|
||||
}
|
||||
|
||||
export function toPluginMessageContext(
|
||||
canonical: CanonicalInboundMessageHookContext | CanonicalSentMessageHookContext,
|
||||
): PluginHookMessageContext {
|
||||
return {
|
||||
const context: PluginHookMessageContext = {
|
||||
channelId: canonical.channelId,
|
||||
accountId: canonical.accountId,
|
||||
conversationId: canonical.conversationId,
|
||||
};
|
||||
if (canonical.sessionKey) {
|
||||
context.sessionKey = canonical.sessionKey;
|
||||
}
|
||||
if (canonical.runId) {
|
||||
context.runId = canonical.runId;
|
||||
}
|
||||
if (canonical.messageId) {
|
||||
context.messageId = canonical.messageId;
|
||||
}
|
||||
if ("senderId" in canonical && canonical.senderId) {
|
||||
context.senderId = canonical.senderId;
|
||||
}
|
||||
assignTraceFields(context, canonical.trace);
|
||||
if (canonical.callDepth != null) {
|
||||
context.callDepth = canonical.callDepth;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function stripChannelPrefix(value: string | undefined, channelId: string): string | undefined {
|
||||
@@ -228,14 +289,19 @@ export function toPluginInboundClaimContext(
|
||||
canonical: CanonicalInboundMessageHookContext,
|
||||
): PluginHookInboundClaimContext {
|
||||
const conversation = resolveInboundConversation(canonical);
|
||||
return {
|
||||
const context: PluginHookInboundClaimContext = {
|
||||
channelId: canonical.channelId,
|
||||
accountId: canonical.accountId,
|
||||
conversationId: conversation.conversationId,
|
||||
sessionKey: canonical.sessionKey,
|
||||
parentConversationId: conversation.parentConversationId,
|
||||
senderId: canonical.senderId,
|
||||
messageId: canonical.messageId,
|
||||
runId: canonical.runId,
|
||||
callDepth: canonical.callDepth,
|
||||
};
|
||||
assignTraceFields(context, canonical.trace);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function toPluginInboundClaimEvent(
|
||||
@@ -246,7 +312,7 @@ export function toPluginInboundClaimEvent(
|
||||
},
|
||||
): PluginHookInboundClaimEvent {
|
||||
const context = toPluginInboundClaimContext(canonical);
|
||||
return {
|
||||
const event: PluginHookInboundClaimEvent = {
|
||||
content: canonical.content,
|
||||
body: canonical.body,
|
||||
bodyForAgent: canonical.bodyForAgent,
|
||||
@@ -261,6 +327,8 @@ export function toPluginInboundClaimEvent(
|
||||
senderUsername: canonical.senderUsername,
|
||||
threadId: canonical.threadId,
|
||||
messageId: canonical.messageId,
|
||||
sessionKey: canonical.sessionKey,
|
||||
runId: canonical.runId,
|
||||
isGroup: canonical.isGroup,
|
||||
commandAuthorized: extras?.commandAuthorized,
|
||||
wasMentioned: extras?.wasMentioned,
|
||||
@@ -284,16 +352,22 @@ export function toPluginInboundClaimEvent(
|
||||
topicName: canonical.topicName,
|
||||
},
|
||||
};
|
||||
assignTraceFields(event, canonical.trace);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function toPluginMessageReceivedEvent(
|
||||
canonical: CanonicalInboundMessageHookContext,
|
||||
): PluginHookMessageReceivedEvent {
|
||||
return {
|
||||
const event: PluginHookMessageReceivedEvent = {
|
||||
from: canonical.from,
|
||||
content: canonical.content,
|
||||
timestamp: canonical.timestamp,
|
||||
threadId: canonical.threadId,
|
||||
messageId: canonical.messageId,
|
||||
senderId: canonical.senderId,
|
||||
sessionKey: canonical.sessionKey,
|
||||
runId: canonical.runId,
|
||||
metadata: {
|
||||
to: canonical.to,
|
||||
provider: canonical.provider,
|
||||
@@ -311,17 +385,24 @@ export function toPluginMessageReceivedEvent(
|
||||
topicName: canonical.topicName,
|
||||
},
|
||||
};
|
||||
assignTraceFields(event, canonical.trace);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function toPluginMessageSentEvent(
|
||||
canonical: CanonicalSentMessageHookContext,
|
||||
): PluginHookMessageSentEvent {
|
||||
return {
|
||||
const event: PluginHookMessageSentEvent = {
|
||||
to: canonical.to,
|
||||
content: canonical.content,
|
||||
success: canonical.success,
|
||||
...(canonical.messageId ? { messageId: canonical.messageId } : {}),
|
||||
...(canonical.sessionKey ? { sessionKey: canonical.sessionKey } : {}),
|
||||
...(canonical.runId ? { runId: canonical.runId } : {}),
|
||||
...(canonical.error ? { error: canonical.error } : {}),
|
||||
};
|
||||
assignTraceFields(event, canonical.trace);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function toInternalMessageReceivedContext(
|
||||
|
||||
@@ -59,6 +59,7 @@ void assertAllPluginPromptMutationResultFieldsListed;
|
||||
// before_agent_start hook (legacy compatibility: combines both phases)
|
||||
export type PluginHookBeforeAgentStartEvent = {
|
||||
prompt: string;
|
||||
runId?: string;
|
||||
/** Optional because legacy hook can run in pre-session phase. */
|
||||
messages?: unknown[];
|
||||
};
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import type { PluginConversationBinding } from "./conversation-binding.types.js";
|
||||
|
||||
export type PluginHookMessageContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
conversationId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
messageId?: string;
|
||||
senderId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
parentSpanId?: string;
|
||||
callDepth?: number;
|
||||
};
|
||||
|
||||
export type PluginHookInboundClaimContext = PluginHookMessageContext & {
|
||||
@@ -28,6 +38,12 @@ export type PluginHookInboundClaimEvent = {
|
||||
senderUsername?: string;
|
||||
threadId?: string | number;
|
||||
messageId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
parentSpanId?: string;
|
||||
isGroup: boolean;
|
||||
commandAuthorized?: boolean;
|
||||
wasMentioned?: boolean;
|
||||
@@ -39,6 +55,14 @@ export type PluginHookMessageReceivedEvent = {
|
||||
content: string;
|
||||
timestamp?: number;
|
||||
threadId?: string | number;
|
||||
messageId?: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
parentSpanId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -59,5 +83,12 @@ export type PluginHookMessageSentEvent = {
|
||||
to: string;
|
||||
content: string;
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
trace?: DiagnosticTraceContext;
|
||||
traceId?: string;
|
||||
spanId?: string;
|
||||
parentSpanId?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@@ -217,6 +217,7 @@ export type PluginHookLlmOutputEvent = {
|
||||
};
|
||||
|
||||
export type PluginHookAgentEndEvent = {
|
||||
runId?: string;
|
||||
messages: unknown[];
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
||||
55
src/plugins/hooks.correlation.test.ts
Normal file
55
src/plugins/hooks.correlation.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createHookRunner } from "./hooks.js";
|
||||
import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
|
||||
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
|
||||
import type { PluginHookRegistration } from "./types.js";
|
||||
|
||||
describe("hook correlation fields", () => {
|
||||
let registry: PluginRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = createEmptyPluginRegistry();
|
||||
});
|
||||
|
||||
it("adds runId to legacy before_agent_start events from hook context", async () => {
|
||||
const handler = vi.fn(() => undefined);
|
||||
addTestHook({
|
||||
registry,
|
||||
pluginId: "plugin-a",
|
||||
hookName: "before_agent_start",
|
||||
handler: handler as PluginHookRegistration["handler"],
|
||||
});
|
||||
|
||||
const runner = createHookRunner(registry);
|
||||
await runner.runBeforeAgentStart({ prompt: "hello" }, TEST_PLUGIN_AGENT_CTX);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ prompt: "hello", runId: "test-run-id" }),
|
||||
TEST_PLUGIN_AGENT_CTX,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds runId to agent_end events from hook context", async () => {
|
||||
const handler = vi.fn(() => undefined);
|
||||
addTestHook({
|
||||
registry,
|
||||
pluginId: "plugin-a",
|
||||
hookName: "agent_end",
|
||||
handler: handler as PluginHookRegistration["handler"],
|
||||
});
|
||||
|
||||
const runner = createHookRunner(registry);
|
||||
await runner.runAgentEnd(
|
||||
{
|
||||
messages: [],
|
||||
success: true,
|
||||
},
|
||||
TEST_PLUGIN_AGENT_CTX,
|
||||
);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ messages: [], success: true, runId: "test-run-id" }),
|
||||
TEST_PLUGIN_AGENT_CTX,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -510,6 +510,16 @@ export function createHookRunner(
|
||||
// Agent Hooks
|
||||
// =========================================================================
|
||||
|
||||
function withAgentRunId<TEvent extends { runId?: string }>(
|
||||
event: TEvent,
|
||||
ctx: PluginHookAgentContext,
|
||||
): TEvent {
|
||||
if (event.runId || !ctx.runId) {
|
||||
return event;
|
||||
}
|
||||
return { ...event, runId: ctx.runId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before_model_resolve hook.
|
||||
* Allows plugins to override provider/model before model resolution.
|
||||
@@ -552,7 +562,7 @@ export function createHookRunner(
|
||||
): Promise<PluginHookBeforeAgentStartResult | undefined> {
|
||||
return runModifyingHook<"before_agent_start", PluginHookBeforeAgentStartResult>(
|
||||
"before_agent_start",
|
||||
event,
|
||||
withAgentRunId(event, ctx),
|
||||
ctx,
|
||||
{
|
||||
mergeResults: (acc, next) => ({
|
||||
@@ -588,7 +598,7 @@ export function createHookRunner(
|
||||
event: PluginHookAgentEndEvent,
|
||||
ctx: PluginHookAgentContext,
|
||||
): Promise<void> {
|
||||
return runVoidHook("agent_end", event, ctx);
|
||||
return runVoidHook("agent_end", withAgentRunId(event, ctx), ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user