mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
feat(plugins): add before agent finalize hook (#71765)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
f5236ba3f34837485d1e319262d4d73ecd46ea8890d3f4c26a069834f376b796 config-baseline.json
|
||||
484b36513ecb4a13cc945c3916fbe5ac712b5e0ab2c4ffa2dc811758da4ec7a6 config-baseline.core.json
|
||||
15a3740b57d0c95f0c0963c1d0eff6d85ecdb8cb03960b4763e847f8a24551c0 config-baseline.json
|
||||
3c39a3a2008ce938886b600e9429a71921c1f9b00c64a16801f47d6d8d2ad7a8 config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
17eb3f8887193579ff32e35f9bd520ba2bd6049e52ab18855c5d41fcbf195d83 config-baseline.plugin.json
|
||||
9e131d7734f8b9cc9e7f8af6cc6b6dc81c9971dc551fadbe66fb0d682173f32d config-baseline.plugin.json
|
||||
|
||||
@@ -126,6 +126,11 @@ Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (pu
|
||||
|
||||
**Compaction events**: `session:compact:before` includes `messageCount`, `tokenCount`. `session:compact:after` adds `compactedCount`, `summaryLength`, `tokensBefore`, `tokensAfter`.
|
||||
|
||||
`command:stop` observes the user issuing `/stop`; it is cancellation/command
|
||||
lifecycle, not an agent-finalization gate. Plugins that need to inspect a
|
||||
natural final answer and ask the agent for one more pass should use the typed
|
||||
plugin hook `before_agent_finalize` instead. See [Plugin hooks](/plugins/hooks).
|
||||
|
||||
## Hook discovery
|
||||
|
||||
Hooks are discovered from these directories, in order of increasing override precedence:
|
||||
|
||||
@@ -219,7 +219,8 @@ For runtime hook debugging:
|
||||
from a module-loaded inspection pass.
|
||||
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway,
|
||||
service/process hints, config path, and RPC health.
|
||||
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `agent_end`) require
|
||||
- Non-bundled conversation hooks (`llm_input`, `llm_output`,
|
||||
`before_agent_finalize`, `agent_end`) require
|
||||
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
|
||||
|
||||
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
|
||||
|
||||
@@ -160,7 +160,7 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- `plugins.entries.<id>.apiKey`: plugin-level API key convenience field (when supported by the plugin).
|
||||
- `plugins.entries.<id>.env`: plugin-scoped env var map.
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks `before_prompt_build` and ignores prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride`. Applies to native plugin hooks and supported bundle-provided hook directories.
|
||||
- `plugins.entries.<id>.hooks.allowConversationAccess`: when `true`, trusted non-bundled plugins may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, and `agent_end`.
|
||||
- `plugins.entries.<id>.hooks.allowConversationAccess`: when `true`, trusted non-bundled plugins may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`.
|
||||
- `plugins.entries.<id>.subagent.allowModelOverride`: explicitly trust this plugin to request per-run `provider` and `model` overrides for background subagent runs.
|
||||
- `plugins.entries.<id>.subagent.allowedModels`: optional allowlist of canonical `provider/model` targets for trusted subagent overrides. Use `"*"` only when you intentionally want to allow any model.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by native OpenClaw plugin schema when available).
|
||||
|
||||
@@ -34,6 +34,7 @@ These are in-process OpenClaw hooks, not Codex `hooks.json` command hooks:
|
||||
- `llm_input`, `llm_output`
|
||||
- `before_tool_call`, `after_tool_call`
|
||||
- `before_message_write` for mirrored transcript records
|
||||
- `before_agent_finalize` through Codex `Stop` relay
|
||||
- `agent_end`
|
||||
|
||||
Plugins can also register runtime-neutral tool-result middleware to rewrite
|
||||
@@ -583,10 +584,10 @@ The Codex harness has three hook layers:
|
||||
|
||||
OpenClaw does not use project or global Codex `hooks.json` files to route
|
||||
OpenClaw plugin behavior. For the supported native tool and permission bridge,
|
||||
OpenClaw injects per-thread Codex config for `PreToolUse`, `PostToolUse`, and
|
||||
`PermissionRequest`. Other Codex hooks such as `SessionStart`,
|
||||
`UserPromptSubmit`, and `Stop` remain Codex-level controls; they are not exposed
|
||||
as OpenClaw plugin hooks in the v1 contract.
|
||||
OpenClaw injects per-thread Codex config for `PreToolUse`, `PostToolUse`,
|
||||
`PermissionRequest`, and `Stop`. Other Codex hooks such as `SessionStart` and
|
||||
`UserPromptSubmit` remain Codex-level controls; they are not exposed as
|
||||
OpenClaw plugin hooks in the v1 contract.
|
||||
|
||||
For OpenClaw dynamic tools, OpenClaw executes the tool after Codex asks for the
|
||||
call, so OpenClaw fires the plugin and middleware behavior it owns in the
|
||||
@@ -622,6 +623,7 @@ Supported in Codex runtime v1:
|
||||
| Context engine lifecycle | Supported | Assemble, ingest or after-turn maintenance, and context-engine compaction coordination run for Codex turns. |
|
||||
| Dynamic tool hooks | Supported | `before_tool_call`, `after_tool_call`, and tool-result middleware run around OpenClaw-owned dynamic tools. |
|
||||
| Lifecycle hooks | Supported as adapter observations | `llm_input`, `llm_output`, `agent_end`, `before_compaction`, and `after_compaction` fire with honest Codex-mode payloads. |
|
||||
| Final-answer revision gate | Supported through the native hook relay | Codex `Stop` is relayed to `before_agent_finalize`; `revise` asks Codex for one more model pass before finalization. |
|
||||
| Native shell, patch, and MCP block or observe | Supported through the native hook relay | Codex `PreToolUse` and `PostToolUse` are relayed for committed native tool surfaces, including MCP payloads on Codex app-server `0.125.0` or newer. Blocking is supported; argument rewriting is not. |
|
||||
| Native permission policy | Supported through the native hook relay | Codex `PermissionRequest` can be routed through OpenClaw policy where the runtime exposes it. If OpenClaw returns no decision, Codex continues through its normal guardian or user approval path. |
|
||||
| App-server trajectory capture | Supported | OpenClaw records the request it sent to app-server and the app-server notifications it receives. |
|
||||
@@ -635,7 +637,6 @@ Not supported in Codex runtime v1:
|
||||
| `tool_result_persist` for Codex-native tool records | That hook transforms OpenClaw-owned transcript writes, not Codex-native tool records. | Could mirror transformed records, but canonical rewrite needs Codex support. |
|
||||
| Rich native compaction metadata | OpenClaw observes compaction start and completion, but does not receive a stable kept/dropped list, token delta, or summary payload. | Needs richer Codex compaction events. |
|
||||
| Compaction intervention | Current OpenClaw compaction hooks are notification-level in Codex mode. | Add Codex pre/post compaction hooks if plugins need to veto or rewrite native compaction. |
|
||||
| Stop or final-answer gating | Codex has native stop hooks, but OpenClaw does not expose final-answer gating as a v1 plugin contract. | Future opt-in primitive with loop and timeout safeguards. |
|
||||
| Byte-for-byte model API request capture | OpenClaw can capture app-server requests and notifications, but Codex core builds the final OpenAI API request internally. | Needs a Codex model-request tracing event or debug API. |
|
||||
|
||||
## Tools, media, and compaction
|
||||
|
||||
@@ -64,6 +64,7 @@ observation-only.
|
||||
- `before_prompt_build` — add dynamic context or system-prompt text before the model call
|
||||
- `before_agent_start` — compatibility-only combined phase; prefer the two hooks above
|
||||
- **`before_agent_reply`** — short-circuit the model turn with a synthetic reply or silence
|
||||
- **`before_agent_finalize`** — inspect the natural final answer and request one more model pass
|
||||
- `agent_end` — observe final messages, success state, and run duration
|
||||
|
||||
**Conversation observation**
|
||||
@@ -185,7 +186,16 @@ bodies, or provider request IDs. These hooks include stable metadata such as
|
||||
`durationMs`/`outcome`, and `upstreamRequestIdHash` when OpenClaw can derive a
|
||||
bounded provider request-id hash.
|
||||
|
||||
Non-bundled plugins that need `llm_input`, `llm_output`, or `agent_end` must set:
|
||||
`before_agent_finalize` runs only when a harness is about to accept a natural
|
||||
final assistant answer. It is not the `/stop` cancellation path and does not
|
||||
run when the user aborts a turn. Return `{ action: "revise", reason }` to ask
|
||||
the harness for one more model pass before finalization, `{ action:
|
||||
"finalize", reason? }` to force finalization, or omit a result to continue.
|
||||
Codex native `Stop` hooks are relayed into this hook as OpenClaw
|
||||
`before_agent_finalize` decisions.
|
||||
|
||||
Non-bundled plugins that need `llm_input`, `llm_output`,
|
||||
`before_agent_finalize`, or `agent_end` must set:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -65,11 +65,12 @@ dynamic tools still execute through OpenClaw, while Codex-native tools such as
|
||||
shell/apply-patch execute inside Codex. For Codex-native tool events, OpenClaw
|
||||
injects a per-turn native hook relay so plugin hooks can block
|
||||
`before_tool_call`, observe `after_tool_call`, and route Codex
|
||||
`PermissionRequest` events through OpenClaw approvals. The v1 relay is
|
||||
deliberately conservative: it does not mutate Codex-native tool arguments,
|
||||
rewrite Codex thread records, or gate final answers/Stop hooks. Use explicit
|
||||
ACP only when you want the ACP runtime/session model. The embedded Codex support
|
||||
boundary is documented in the
|
||||
`PermissionRequest` events through OpenClaw approvals. Codex `Stop` hooks are
|
||||
relayed to OpenClaw `before_agent_finalize`, where plugins can request one more
|
||||
model pass before Codex finalizes its answer. The relay remains deliberately
|
||||
conservative: it does not mutate Codex-native tool arguments or rewrite Codex
|
||||
thread records. Use explicit ACP only when you want the ACP runtime/session
|
||||
model. The embedded Codex support boundary is documented in the
|
||||
[Codex harness v1 support contract](/plugins/codex-harness#v1-support-contract).
|
||||
|
||||
Natural-language triggers that should route to the ACP runtime:
|
||||
|
||||
@@ -223,7 +223,7 @@ do not run in live chat traffic, check these first:
|
||||
`openclaw gateway run` process.
|
||||
- Use `openclaw plugins inspect <id> --json` to confirm hook registrations and
|
||||
diagnostics. Non-bundled conversation hooks such as `llm_input`,
|
||||
`llm_output`, and `agent_end` need
|
||||
`llm_output`, `before_agent_finalize`, and `agent_end` need
|
||||
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
|
||||
- For model switching, prefer `before_model_resolve`. It runs before model
|
||||
resolution for agent turns; `llm_output` only runs after a model attempt
|
||||
|
||||
@@ -59,11 +59,25 @@ describe("Codex native hook relay config", () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
"hooks.Stop": [
|
||||
{
|
||||
matcher: null,
|
||||
hooks: [
|
||||
{
|
||||
type: "command",
|
||||
command:
|
||||
"openclaw hooks relay --provider codex --relay-id relay-1 --event before_agent_finalize",
|
||||
timeout: 7,
|
||||
async: false,
|
||||
statusMessage: "OpenClaw native hook relay",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(JSON.stringify(config)).not.toContain("timeoutSec");
|
||||
expect(config).not.toHaveProperty("hooks.SessionStart");
|
||||
expect(config).not.toHaveProperty("hooks.UserPromptSubmit");
|
||||
expect(config).not.toHaveProperty("hooks.Stop");
|
||||
});
|
||||
|
||||
it("includes only requested hook events", () => {
|
||||
@@ -108,6 +122,7 @@ describe("Codex native hook relay config", () => {
|
||||
"hooks.PreToolUse": [],
|
||||
"hooks.PostToolUse": [],
|
||||
"hooks.PermissionRequest": [],
|
||||
"hooks.Stop": [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -119,7 +134,7 @@ function createRelay(): NativeHookRelayRegistrationHandle {
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
allowedEvents: ["pre_tool_use", "post_tool_use", "permission_request"],
|
||||
allowedEvents: ["pre_tool_use", "post_tool_use", "permission_request", "before_agent_finalize"],
|
||||
expiresAtMs: Date.now() + 1000,
|
||||
commandForEvent: (event) =>
|
||||
`openclaw hooks relay --provider codex --relay-id relay-1 --event ${event}`,
|
||||
|
||||
@@ -8,14 +8,16 @@ export const CODEX_NATIVE_HOOK_RELAY_EVENTS = [
|
||||
"pre_tool_use",
|
||||
"post_tool_use",
|
||||
"permission_request",
|
||||
"before_agent_finalize",
|
||||
] as const satisfies readonly NativeHookRelayEvent[];
|
||||
|
||||
type CodexHookEventName = "PreToolUse" | "PostToolUse" | "PermissionRequest";
|
||||
type CodexHookEventName = "PreToolUse" | "PostToolUse" | "PermissionRequest" | "Stop";
|
||||
|
||||
const CODEX_HOOK_EVENT_BY_NATIVE_EVENT: Record<NativeHookRelayEvent, CodexHookEventName> = {
|
||||
pre_tool_use: "PreToolUse",
|
||||
post_tool_use: "PostToolUse",
|
||||
permission_request: "PermissionRequest",
|
||||
before_agent_finalize: "Stop",
|
||||
};
|
||||
|
||||
export function buildCodexNativeHookRelayConfig(params: {
|
||||
@@ -53,6 +55,7 @@ export function buildCodexNativeHookRelayDisabledConfig(): JsonObject {
|
||||
"hooks.PreToolUse": [],
|
||||
"hooks.PostToolUse": [],
|
||||
"hooks.PermissionRequest": [],
|
||||
"hooks.Stop": [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -570,6 +570,7 @@ describe("runCodexAppServerAttempt", () => {
|
||||
"hooks.PreToolUse": [],
|
||||
"hooks.PostToolUse": [],
|
||||
"hooks.PermissionRequest": [],
|
||||
"hooks.Stop": [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ export type AgentHarnessHookContext = {
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
workspaceDir?: string;
|
||||
modelProviderId?: string;
|
||||
modelId?: string;
|
||||
messageProvider?: string;
|
||||
trigger?: string;
|
||||
channelId?: string;
|
||||
@@ -18,6 +20,8 @@ export function buildAgentHookContext(params: AgentHarnessHookContext): PluginHo
|
||||
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
||||
...(params.sessionId ? { sessionId: params.sessionId } : {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
...(params.modelProviderId ? { modelProviderId: params.modelProviderId } : {}),
|
||||
...(params.modelId ? { modelId: params.modelId } : {}),
|
||||
...(params.messageProvider ? { messageProvider: params.messageProvider } : {}),
|
||||
...(params.trigger ? { trigger: params.trigger } : {}),
|
||||
...(params.channelId ? { channelId: params.channelId } : {}),
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import type {
|
||||
PluginHookAgentEndEvent,
|
||||
PluginHookBeforeAgentFinalizeEvent,
|
||||
PluginHookBeforeAgentFinalizeResult,
|
||||
PluginHookLlmInputEvent,
|
||||
PluginHookLlmOutputEvent,
|
||||
} from "../../plugins/hook-types.js";
|
||||
@@ -52,3 +54,42 @@ export function runAgentHarnessAgentEndHook(params: {
|
||||
log.warn(`agent_end hook failed: ${String(error)}`);
|
||||
});
|
||||
}
|
||||
|
||||
export type AgentHarnessBeforeAgentFinalizeOutcome =
|
||||
| { action: "continue" }
|
||||
| { action: "revise"; reason: string }
|
||||
| { action: "finalize"; reason?: string };
|
||||
|
||||
export async function runAgentHarnessBeforeAgentFinalizeHook(params: {
|
||||
event: PluginHookBeforeAgentFinalizeEvent;
|
||||
ctx: AgentHarnessHookContext;
|
||||
hookRunner?: AgentHarnessHookRunner;
|
||||
}): Promise<AgentHarnessBeforeAgentFinalizeOutcome> {
|
||||
const hookRunner = params.hookRunner ?? getGlobalHookRunner();
|
||||
if (!hookRunner?.hasHooks("before_agent_finalize")) {
|
||||
return { action: "continue" };
|
||||
}
|
||||
try {
|
||||
return normalizeBeforeAgentFinalizeResult(
|
||||
await hookRunner.runBeforeAgentFinalize(params.event, buildAgentHookContext(params.ctx)),
|
||||
);
|
||||
} catch (error) {
|
||||
log.warn(`before_agent_finalize hook failed: ${String(error)}`);
|
||||
return { action: "continue" };
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBeforeAgentFinalizeResult(
|
||||
result: PluginHookBeforeAgentFinalizeResult | undefined,
|
||||
): AgentHarnessBeforeAgentFinalizeOutcome {
|
||||
if (result?.action === "finalize") {
|
||||
return result.reason?.trim()
|
||||
? { action: "finalize", reason: result.reason.trim() }
|
||||
: { action: "finalize" };
|
||||
}
|
||||
if (result?.action === "revise") {
|
||||
const reason = result.reason?.trim();
|
||||
return reason ? { action: "revise", reason } : { action: "continue" };
|
||||
}
|
||||
return { action: "continue" };
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ describe("native hook relay registry", () => {
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
for (const event of ["pre_tool_use", "post_tool_use"] as const) {
|
||||
for (const event of ["pre_tool_use", "post_tool_use", "before_agent_finalize"] as const) {
|
||||
await expect(
|
||||
invokeNativeHookRelay({
|
||||
provider: "codex",
|
||||
@@ -709,6 +709,108 @@ describe("native hook relay registry", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps Codex Stop to before_agent_finalize revision output", async () => {
|
||||
const beforeAgentFinalize = vi.fn(async () => ({
|
||||
action: "revise",
|
||||
reason: "please run the focused tests before finalizing",
|
||||
}));
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{ hookName: "before_agent_finalize", handler: beforeAgentFinalize },
|
||||
]),
|
||||
);
|
||||
const relay = registerNativeHookRelay({
|
||||
provider: "codex",
|
||||
agentId: "agent-1",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const response = await invokeNativeHookRelay({
|
||||
provider: "codex",
|
||||
relayId: relay.relayId,
|
||||
event: "before_agent_finalize",
|
||||
rawPayload: {
|
||||
hook_event_name: "Stop",
|
||||
session_id: "codex-session-1",
|
||||
turn_id: "turn-1",
|
||||
cwd: "/repo",
|
||||
transcript_path: "/tmp/session.jsonl",
|
||||
model: "gpt-5.4",
|
||||
permission_mode: "workspace-write",
|
||||
stop_hook_active: true,
|
||||
last_assistant_message: "done",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
stdout: `${JSON.stringify({
|
||||
decision: "block",
|
||||
reason: "please run the focused tests before finalizing",
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
});
|
||||
expect(beforeAgentFinalize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
turnId: "turn-1",
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
cwd: "/repo",
|
||||
transcriptPath: "/tmp/session.jsonl",
|
||||
stopHookActive: true,
|
||||
lastAssistantMessage: "done",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
agentId: "agent-1",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
runId: "run-1",
|
||||
workspaceDir: "/repo",
|
||||
modelId: "gpt-5.4",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps before_agent_finalize finalize output to Codex continue false", async () => {
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{
|
||||
hookName: "before_agent_finalize",
|
||||
handler: vi.fn(async () => ({ action: "finalize", reason: "already checked" })),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const relay = registerNativeHookRelay({
|
||||
provider: "codex",
|
||||
sessionId: "session-1",
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const response = await invokeNativeHookRelay({
|
||||
provider: "codex",
|
||||
relayId: relay.relayId,
|
||||
event: "before_agent_finalize",
|
||||
rawPayload: {
|
||||
hook_event_name: "Stop",
|
||||
stop_hook_active: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toEqual({
|
||||
stdout: `${JSON.stringify({
|
||||
continue: false,
|
||||
stopReason: "already checked",
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps PermissionRequest approval allow and deny decisions to Codex hook output", async () => {
|
||||
const relay = registerNativeHookRelay({
|
||||
provider: "codex",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { runBeforeToolCallHook } from "../pi-tools.before-tool-call.js";
|
||||
import { normalizeToolName } from "../tool-policy.js";
|
||||
import { callGatewayTool } from "../tools/gateway.js";
|
||||
import { runAgentHarnessAfterToolCallHook } from "./hook-helpers.js";
|
||||
import { runAgentHarnessBeforeAgentFinalizeHook } from "./lifecycle-hook-helpers.js";
|
||||
|
||||
export type JsonValue =
|
||||
| null
|
||||
@@ -20,6 +21,7 @@ export const NATIVE_HOOK_RELAY_EVENTS = [
|
||||
"pre_tool_use",
|
||||
"post_tool_use",
|
||||
"permission_request",
|
||||
"before_agent_finalize",
|
||||
] as const;
|
||||
|
||||
export const NATIVE_HOOK_RELAY_PROVIDERS = ["codex"] as const;
|
||||
@@ -38,6 +40,11 @@ export type NativeHookRelayInvocation = {
|
||||
runId: string;
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
turnId?: string;
|
||||
transcriptPath?: string;
|
||||
permissionMode?: string;
|
||||
stopHookActive?: boolean;
|
||||
lastAssistantMessage?: string;
|
||||
toolName?: string;
|
||||
toolUseId?: string;
|
||||
rawPayload: JsonValue;
|
||||
@@ -93,7 +100,19 @@ export type InvokeNativeHookRelayParams = {
|
||||
};
|
||||
|
||||
type NativeHookRelayInvocationMetadata = Partial<
|
||||
Pick<NativeHookRelayInvocation, "nativeEventName" | "cwd" | "model" | "toolName" | "toolUseId">
|
||||
Pick<
|
||||
NativeHookRelayInvocation,
|
||||
| "nativeEventName"
|
||||
| "cwd"
|
||||
| "model"
|
||||
| "turnId"
|
||||
| "transcriptPath"
|
||||
| "permissionMode"
|
||||
| "stopHookActive"
|
||||
| "lastAssistantMessage"
|
||||
| "toolName"
|
||||
| "toolUseId"
|
||||
>
|
||||
>;
|
||||
|
||||
type NativeHookRelayProviderAdapter = {
|
||||
@@ -102,6 +121,8 @@ type NativeHookRelayProviderAdapter = {
|
||||
readToolResponse: (rawPayload: JsonValue) => unknown;
|
||||
renderNoopResponse: (event: NativeHookRelayEvent) => NativeHookRelayProcessResponse;
|
||||
renderPreToolUseBlockResponse: (reason: string) => NativeHookRelayProcessResponse;
|
||||
renderBeforeAgentFinalizeReviseResponse: (reason: string) => NativeHookRelayProcessResponse;
|
||||
renderBeforeAgentFinalizeStopResponse: (reason?: string) => NativeHookRelayProcessResponse;
|
||||
renderPermissionDecisionResponse: (
|
||||
decision: NativeHookRelayPermissionDecision,
|
||||
message?: string,
|
||||
@@ -185,6 +206,22 @@ const nativeHookRelayProviderAdapters: Record<
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
}),
|
||||
renderBeforeAgentFinalizeReviseResponse: (reason) => ({
|
||||
stdout: `${JSON.stringify({
|
||||
decision: "block",
|
||||
reason,
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
}),
|
||||
renderBeforeAgentFinalizeStopResponse: (reason) => ({
|
||||
stdout: `${JSON.stringify({
|
||||
continue: false,
|
||||
...(reason?.trim() ? { stopReason: reason.trim() } : {}),
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
}),
|
||||
renderPermissionDecisionResponse: (decision, message) => ({
|
||||
stdout: `${JSON.stringify({
|
||||
hookSpecificOutput: {
|
||||
@@ -367,6 +404,9 @@ async function processNativeHookRelayInvocation(params: {
|
||||
if (params.invocation.event === "post_tool_use") {
|
||||
return runNativeHookRelayPostToolUse(params);
|
||||
}
|
||||
if (params.invocation.event === "before_agent_finalize") {
|
||||
return runNativeHookRelayBeforeAgentFinalize(params);
|
||||
}
|
||||
return runNativeHookRelayPermissionRequest(params);
|
||||
}
|
||||
|
||||
@@ -464,6 +504,46 @@ async function runNativeHookRelayPermissionRequest(params: {
|
||||
return params.adapter.renderNoopResponse(params.invocation.event);
|
||||
}
|
||||
|
||||
async function runNativeHookRelayBeforeAgentFinalize(params: {
|
||||
registration: NativeHookRelayRegistration;
|
||||
invocation: NativeHookRelayInvocation;
|
||||
adapter: NativeHookRelayProviderAdapter;
|
||||
}): Promise<NativeHookRelayProcessResponse> {
|
||||
const outcome = await runAgentHarnessBeforeAgentFinalizeHook({
|
||||
event: {
|
||||
runId: params.registration.runId,
|
||||
sessionId: params.registration.sessionId,
|
||||
...(params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {}),
|
||||
...(params.invocation.turnId ? { turnId: params.invocation.turnId } : {}),
|
||||
provider: params.registration.provider,
|
||||
...(params.invocation.model ? { model: params.invocation.model } : {}),
|
||||
...(params.invocation.cwd ? { cwd: params.invocation.cwd } : {}),
|
||||
...(params.invocation.transcriptPath
|
||||
? { transcriptPath: params.invocation.transcriptPath }
|
||||
: {}),
|
||||
stopHookActive: params.invocation.stopHookActive === true,
|
||||
...(params.invocation.lastAssistantMessage
|
||||
? { lastAssistantMessage: params.invocation.lastAssistantMessage }
|
||||
: {}),
|
||||
},
|
||||
ctx: {
|
||||
...(params.registration.agentId ? { agentId: params.registration.agentId } : {}),
|
||||
sessionId: params.registration.sessionId,
|
||||
...(params.registration.sessionKey ? { sessionKey: params.registration.sessionKey } : {}),
|
||||
runId: params.registration.runId,
|
||||
...(params.invocation.cwd ? { workspaceDir: params.invocation.cwd } : {}),
|
||||
...(params.invocation.model ? { modelId: params.invocation.model } : {}),
|
||||
},
|
||||
});
|
||||
if (outcome.action === "revise") {
|
||||
return params.adapter.renderBeforeAgentFinalizeReviseResponse(outcome.reason);
|
||||
}
|
||||
if (outcome.action === "finalize") {
|
||||
return params.adapter.renderBeforeAgentFinalizeStopResponse(outcome.reason);
|
||||
}
|
||||
return params.adapter.renderNoopResponse(params.invocation.event);
|
||||
}
|
||||
|
||||
async function startNativeHookRelayPermissionApprovalWithBudget(params: {
|
||||
registration: NativeHookRelayRegistration;
|
||||
approvalKey: string;
|
||||
@@ -720,6 +800,26 @@ function normalizeCodexHookMetadata(rawPayload: JsonValue): NativeHookRelayInvoc
|
||||
if (model) {
|
||||
metadata.model = model;
|
||||
}
|
||||
const turnId = readOptionalString(payload.turn_id);
|
||||
if (turnId) {
|
||||
metadata.turnId = turnId;
|
||||
}
|
||||
const transcriptPath = readOptionalString(payload.transcript_path);
|
||||
if (transcriptPath) {
|
||||
metadata.transcriptPath = transcriptPath;
|
||||
}
|
||||
const permissionMode = readOptionalString(payload.permission_mode);
|
||||
if (permissionMode) {
|
||||
metadata.permissionMode = permissionMode;
|
||||
}
|
||||
const stopHookActive = readOptionalBoolean(payload.stop_hook_active);
|
||||
if (stopHookActive !== undefined) {
|
||||
metadata.stopHookActive = stopHookActive;
|
||||
}
|
||||
const lastAssistantMessage = readOptionalString(payload.last_assistant_message);
|
||||
if (lastAssistantMessage) {
|
||||
metadata.lastAssistantMessage = lastAssistantMessage;
|
||||
}
|
||||
const toolName = readOptionalString(payload.tool_name);
|
||||
if (toolName) {
|
||||
metadata.toolName = toolName;
|
||||
@@ -950,7 +1050,12 @@ function readNativeHookRelayProvider(value: unknown): NativeHookRelayProvider {
|
||||
}
|
||||
|
||||
function readNativeHookRelayEvent(value: unknown): NativeHookRelayEvent {
|
||||
if (value === "pre_tool_use" || value === "post_tool_use" || value === "permission_request") {
|
||||
if (
|
||||
value === "pre_tool_use" ||
|
||||
value === "post_tool_use" ||
|
||||
value === "permission_request" ||
|
||||
value === "before_agent_finalize"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
throw new Error("unsupported native hook relay event");
|
||||
@@ -967,6 +1072,10 @@ function readOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function readOptionalBoolean(value: unknown): boolean | undefined {
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function isJsonValue(value: unknown): value is JsonValue {
|
||||
const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
|
||||
let nodes = 0;
|
||||
|
||||
@@ -186,4 +186,26 @@ describe("native hook relay CLI", () => {
|
||||
expect(stdout.text()).toBe("");
|
||||
expect(stderr.text()).toContain("native hook relay unavailable");
|
||||
});
|
||||
|
||||
it("keeps before_agent_finalize unavailable handling observational", async () => {
|
||||
const callGateway = vi.fn(async () => {
|
||||
throw new Error("gateway closed");
|
||||
});
|
||||
const stdout = createWritableTextBuffer();
|
||||
const stderr = createWritableTextBuffer();
|
||||
|
||||
const exitCode = await runNativeHookRelayCli(
|
||||
{ provider: "codex", relayId: "relay-1", event: "before_agent_finalize" },
|
||||
{
|
||||
stdin: createReadableTextStream("{}"),
|
||||
stdout,
|
||||
stderr,
|
||||
callGateway: callGateway as never,
|
||||
},
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.text()).toBe("");
|
||||
expect(stderr.text()).toContain("native hook relay unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22974,7 +22974,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
type: "boolean",
|
||||
title: "Allow Conversation Access Hooks",
|
||||
description:
|
||||
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
@@ -27491,7 +27491,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
},
|
||||
"plugins.entries.*.hooks.allowConversationAccess": {
|
||||
label: "Allow Conversation Access Hooks",
|
||||
help: "Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
help: "Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
tags: ["access"],
|
||||
},
|
||||
"plugins.entries.*.hooks.allowPromptInjection": {
|
||||
|
||||
@@ -768,6 +768,7 @@ describe("config help copy quality", () => {
|
||||
const pluginConversationPolicy = FIELD_HELP["plugins.entries.*.hooks.allowConversationAccess"];
|
||||
expect(pluginConversationPolicy.includes("llm_input")).toBe(true);
|
||||
expect(pluginConversationPolicy.includes("llm_output")).toBe(true);
|
||||
expect(pluginConversationPolicy.includes("before_agent_finalize")).toBe(true);
|
||||
expect(pluginConversationPolicy.includes("agent_end")).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1157,7 +1157,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"plugins.entries.*.hooks.allowPromptInjection":
|
||||
"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
|
||||
"plugins.entries.*.hooks.allowConversationAccess":
|
||||
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
"Controls whether this plugin may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_agent_finalize`, and `agent_end`. Non-bundled plugins must opt in explicitly.",
|
||||
"plugins.entries.*.subagent":
|
||||
"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.",
|
||||
"plugins.entries.*.subagent.allowModelOverride":
|
||||
|
||||
@@ -116,6 +116,7 @@ export {
|
||||
runAgentHarnessBeforeMessageWriteHook,
|
||||
} from "../agents/harness/hook-helpers.js";
|
||||
export {
|
||||
runAgentHarnessBeforeAgentFinalizeHook,
|
||||
runAgentHarnessAgentEndHook,
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
|
||||
@@ -63,6 +63,7 @@ export type PluginHookName =
|
||||
| "model_call_ended"
|
||||
| "llm_input"
|
||||
| "llm_output"
|
||||
| "before_agent_finalize"
|
||||
| "agent_end"
|
||||
| "before_compaction"
|
||||
| "after_compaction"
|
||||
@@ -96,6 +97,7 @@ export const PLUGIN_HOOK_NAMES = [
|
||||
"model_call_ended",
|
||||
"llm_input",
|
||||
"llm_output",
|
||||
"before_agent_finalize",
|
||||
"agent_end",
|
||||
"before_compaction",
|
||||
"after_compaction",
|
||||
@@ -146,6 +148,7 @@ export const isPromptInjectionHookName = (hookName: PluginHookName): boolean =>
|
||||
export const CONVERSATION_HOOK_NAMES = [
|
||||
"llm_input",
|
||||
"llm_output",
|
||||
"before_agent_finalize",
|
||||
"agent_end",
|
||||
] as const satisfies readonly PluginHookName[];
|
||||
|
||||
@@ -248,6 +251,30 @@ export type PluginHookAgentEndEvent = {
|
||||
durationMs?: number;
|
||||
};
|
||||
|
||||
export type PluginHookBeforeAgentFinalizeEvent = {
|
||||
runId?: string;
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
turnId?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
cwd?: string;
|
||||
transcriptPath?: string;
|
||||
stopHookActive: boolean;
|
||||
lastAssistantMessage?: string;
|
||||
messages?: unknown[];
|
||||
};
|
||||
|
||||
export type PluginHookBeforeAgentFinalizeResult = {
|
||||
/**
|
||||
* continue: accept normal finalization.
|
||||
* revise: block finalization and ask the harness for another model pass.
|
||||
* finalize: force finalization even if another hook requested revision.
|
||||
*/
|
||||
action?: "continue" | "revise" | "finalize";
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type PluginHookBeforeCompactionEvent = {
|
||||
messageCount: number;
|
||||
compactingCount?: number;
|
||||
@@ -713,6 +740,13 @@ export type PluginHookHandlerMap = {
|
||||
event: PluginHookLlmOutputEvent,
|
||||
ctx: PluginHookAgentContext,
|
||||
) => Promise<void> | void;
|
||||
before_agent_finalize: (
|
||||
event: PluginHookBeforeAgentFinalizeEvent,
|
||||
ctx: PluginHookAgentContext,
|
||||
) =>
|
||||
| Promise<PluginHookBeforeAgentFinalizeResult | void>
|
||||
| PluginHookBeforeAgentFinalizeResult
|
||||
| void;
|
||||
agent_end: (event: PluginHookAgentEndEvent, ctx: PluginHookAgentContext) => Promise<void> | void;
|
||||
before_compaction: (
|
||||
event: PluginHookBeforeCompactionEvent,
|
||||
|
||||
91
src/plugins/hooks.before-agent-finalize.test.ts
Normal file
91
src/plugins/hooks.before-agent-finalize.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createHookRunner } from "./hooks.js";
|
||||
import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
|
||||
|
||||
const EVENT = {
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:session-1",
|
||||
turnId: "turn-1",
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
cwd: "/repo",
|
||||
transcriptPath: "/tmp/session.jsonl",
|
||||
stopHookActive: false,
|
||||
lastAssistantMessage: "done",
|
||||
};
|
||||
|
||||
describe("before_agent_finalize hook runner", () => {
|
||||
it("returns undefined when no hooks are registered", async () => {
|
||||
const runner = createHookRunner(createMockPluginRegistry([]));
|
||||
|
||||
await expect(
|
||||
runner.runBeforeAgentFinalize(EVENT, TEST_PLUGIN_AGENT_CTX),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns a revise decision with the hook reason", async () => {
|
||||
const handler = vi.fn().mockResolvedValue({
|
||||
action: "revise",
|
||||
reason: "run the focused tests before finalizing",
|
||||
});
|
||||
const runner = createHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "before_agent_finalize", handler }]),
|
||||
);
|
||||
|
||||
await expect(runner.runBeforeAgentFinalize(EVENT, TEST_PLUGIN_AGENT_CTX)).resolves.toEqual({
|
||||
action: "revise",
|
||||
reason: "run the focused tests before finalizing",
|
||||
});
|
||||
expect(handler).toHaveBeenCalledWith(EVENT, TEST_PLUGIN_AGENT_CTX);
|
||||
});
|
||||
|
||||
it("joins multiple revise reasons so the harness can request one follow-up pass", async () => {
|
||||
const runner = createHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{
|
||||
hookName: "before_agent_finalize",
|
||||
handler: vi.fn().mockResolvedValue({ action: "revise", reason: "fix lint" }),
|
||||
},
|
||||
{
|
||||
hookName: "before_agent_finalize",
|
||||
handler: vi.fn().mockResolvedValue({ action: "revise", reason: "then rerun tests" }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await expect(runner.runBeforeAgentFinalize(EVENT, TEST_PLUGIN_AGENT_CTX)).resolves.toEqual({
|
||||
action: "revise",
|
||||
reason: "fix lint\n\nthen rerun tests",
|
||||
});
|
||||
});
|
||||
|
||||
it("lets finalize override earlier revise decisions", async () => {
|
||||
const runner = createHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{
|
||||
hookName: "before_agent_finalize",
|
||||
handler: vi.fn().mockResolvedValue({ action: "revise", reason: "keep going" }),
|
||||
},
|
||||
{
|
||||
hookName: "before_agent_finalize",
|
||||
handler: vi.fn().mockResolvedValue({ action: "finalize", reason: "enough" }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await expect(runner.runBeforeAgentFinalize(EVENT, TEST_PLUGIN_AGENT_CTX)).resolves.toEqual({
|
||||
action: "finalize",
|
||||
reason: "enough",
|
||||
});
|
||||
});
|
||||
|
||||
it("hasHooks reports correctly", () => {
|
||||
const runner = createHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "before_agent_finalize", handler: vi.fn() }]),
|
||||
);
|
||||
|
||||
expect(runner.hasHooks("before_agent_finalize")).toBe(true);
|
||||
expect(runner.hasHooks("agent_end")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
PluginHookAfterToolCallEvent,
|
||||
PluginHookAgentContext,
|
||||
PluginHookAgentEndEvent,
|
||||
PluginHookBeforeAgentFinalizeEvent,
|
||||
PluginHookBeforeAgentFinalizeResult,
|
||||
PluginHookBeforeAgentReplyEvent,
|
||||
PluginHookBeforeAgentReplyResult,
|
||||
PluginHookBeforeAgentStartEvent,
|
||||
@@ -91,6 +93,8 @@ export type {
|
||||
PluginHookModelCallStartedEvent,
|
||||
PluginHookLlmInputEvent,
|
||||
PluginHookLlmOutputEvent,
|
||||
PluginHookBeforeAgentFinalizeEvent,
|
||||
PluginHookBeforeAgentFinalizeResult,
|
||||
PluginHookAgentEndEvent,
|
||||
PluginHookBeforeCompactionEvent,
|
||||
PluginHookBeforeResetEvent,
|
||||
@@ -253,6 +257,34 @@ export function createHookRunner(
|
||||
}),
|
||||
});
|
||||
|
||||
const mergeBeforeAgentFinalize = (
|
||||
acc: PluginHookBeforeAgentFinalizeResult | undefined,
|
||||
next: PluginHookBeforeAgentFinalizeResult,
|
||||
): PluginHookBeforeAgentFinalizeResult => {
|
||||
if (acc?.action === "finalize") {
|
||||
return acc;
|
||||
}
|
||||
if (next.action === "finalize") {
|
||||
return { action: "finalize", reason: next.reason };
|
||||
}
|
||||
if (acc?.action === "revise" && next.action === "revise") {
|
||||
return {
|
||||
action: "revise",
|
||||
reason: concatOptionalTextSegments({
|
||||
left: acc.reason,
|
||||
right: next.reason,
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (acc?.action === "revise") {
|
||||
return acc;
|
||||
}
|
||||
if (next.action === "revise") {
|
||||
return { action: "revise", reason: next.reason };
|
||||
}
|
||||
return next.action === "continue" ? { action: "continue", reason: next.reason } : (acc ?? next);
|
||||
};
|
||||
|
||||
const mergeSubagentSpawningResult = (
|
||||
acc: PluginHookSubagentSpawningResult | undefined,
|
||||
next: PluginHookSubagentSpawningResult,
|
||||
@@ -646,6 +678,23 @@ export function createHookRunner(
|
||||
return runVoidHook("llm_output", event, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before_agent_finalize hook.
|
||||
* Allows plugins to request one more model pass before a natural final reply
|
||||
* is accepted. This is not the user-facing /stop cancellation path.
|
||||
*/
|
||||
async function runBeforeAgentFinalize(
|
||||
event: PluginHookBeforeAgentFinalizeEvent,
|
||||
ctx: PluginHookAgentContext,
|
||||
): Promise<PluginHookBeforeAgentFinalizeResult | undefined> {
|
||||
return runModifyingHook<"before_agent_finalize", PluginHookBeforeAgentFinalizeResult>(
|
||||
"before_agent_finalize",
|
||||
withAgentRunId(event, ctx),
|
||||
ctx,
|
||||
{ mergeResults: mergeBeforeAgentFinalize },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before_compaction hook.
|
||||
*/
|
||||
@@ -1156,6 +1205,7 @@ export function createHookRunner(
|
||||
runModelCallEnded,
|
||||
runLlmInput,
|
||||
runLlmOutput,
|
||||
runBeforeAgentFinalize,
|
||||
runAgentEnd,
|
||||
runBeforeCompaction,
|
||||
runAfterCompaction,
|
||||
|
||||
@@ -5880,6 +5880,7 @@ module.exports = {
|
||||
body: `module.exports = { id: "conversation-hooks", register(api) {
|
||||
api.on("llm_input", () => undefined);
|
||||
api.on("llm_output", () => undefined);
|
||||
api.on("before_agent_finalize", () => undefined);
|
||||
api.on("agent_end", () => undefined);
|
||||
} };`,
|
||||
});
|
||||
@@ -5897,7 +5898,7 @@ module.exports = {
|
||||
"non-bundled plugins must set plugins.entries.conversation-hooks.hooks.allowConversationAccess=true",
|
||||
),
|
||||
);
|
||||
expect(blockedDiagnostics).toHaveLength(3);
|
||||
expect(blockedDiagnostics).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("allows conversation typed hooks for non-bundled plugins when explicitly enabled", () => {
|
||||
@@ -5908,6 +5909,7 @@ module.exports = {
|
||||
body: `module.exports = { id: "conversation-hooks-allowed", register(api) {
|
||||
api.on("llm_input", () => undefined);
|
||||
api.on("llm_output", () => undefined);
|
||||
api.on("before_agent_finalize", () => undefined);
|
||||
api.on("agent_end", () => undefined);
|
||||
} };`,
|
||||
});
|
||||
@@ -5929,6 +5931,7 @@ module.exports = {
|
||||
expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
|
||||
"llm_input",
|
||||
"llm_output",
|
||||
"before_agent_finalize",
|
||||
"agent_end",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user